Getting started#
The Dashboard
The Dashboard is your landing page. It shows quick stats from every part of the plugin: content graph health, managed link counts, click totals, auto-link status, and SEO issues. Each card links to the relevant detail page.
How the pieces fit together
- Content Graph: visualizes your site's internal link structure as an interactive network. Detects SEO issues like broken links, orphan pages, and link hoarders.
- Link Builder: canvas-first editor where you plan internal-link silos visually and commit them as draft posts with the link skeleton already wired in. Quick-start templates for silos, hub-and-pillars, topical clusters.
- Managed Links: create tracked redirect URLs (e.g.
/node/my-link) with click logging, sub-ID tracking, and scheduling. - Auto-Link: automatically replace keywords in your content with managed link URLs on the frontend.
- Shorteners: connect external URL shortening services (Bit.ly, Short.io, etc.) to generate short URLs for your managed links.
- Analytics: click trends, top links, geographic breakdown, and per-link detail views.
- Search Performance: current GSC snapshot for the property: per-page metrics + top queries table. Honours the country filter under Settings → Google Integrations.
- Rank Tracker: historical time series of Google Search Console per-query ranks. Opt-in module that reuses the existing GSC sync, so it needs no third-party SERP API. Shows which keywords moved, in which direction, and flags pages that crossed the alert threshold.
Quick start
- Go to Content Graph and click "Load Map" (or generate demo data to test)
- Go to Manage Links → "Add New Link" to create your first tracked redirect
- Add keywords to a managed link to enable auto-linking
- Check Analytics after some clicks to see the data
Demo data
The Content Graph page has a "Generate Sample Data" tool at the bottom. It creates realistic test posts with proper link structure, plus sample managed links with click data and keywords. Use "Clean up" to remove everything when you're done testing.
Content Graph#
The content graph visualizes your site's internal link structure as an interactive network. Each node is a page or post; each edge is a link from one page to another.
The 5-level system
- Level 1, Hub: The most-connected page in each connected component. Usually your main cornerstone content.
- Level 2, Pillar: Pages directly linked from the hub. Your main category-level pages.
- Level 3, Cluster: One step further, supporting content that fleshes out a topic.
- Level 4, Long Tails: Pages at the edges with fewer incoming links.
- Level 5, Orphan: Pages with no internal links at all. Often missed by crawlers.
Level names are customizable in Settings → Content Graph → Level Names.
Link Gravity
Link Gravity is the plugin's name for iterative link authority scoring (the same algorithm as PageRank). Nodes are sized by their Link Gravity score, bigger nodes have more authority flowing to them. Configure damping factor and iterations in Settings.
SEO error detection
- Broken Links: red dashed edges pointing to URLs that don't resolve to an active post
- Ghost Pages: purple nodes found in your sitemap but with zero internal links
- Content Gaps: yellow dashed L2 nodes with too few child pages
- Link Hoarders: orange border nodes with many inbound links but zero outbound
- Cross-Cluster: orange dashed edges linking unrelated topic clusters
- Nofollow Links: green dashed edges where rel="nofollow" is set
- Redirect Chains: purple dashed edges where the link URL redirects
- Link Heavy Pages: pages with 100+ outgoing links (configurable)
- Duplicate Titles: magenta border on pages sharing the same title
- Thin Content: amber badge on pages below the word count threshold
- Over-Optimized Anchors: warning when 80%+ of inbound links use the same anchor text
- Managed Links: amber edges for links that route through your
/node/redirects
Keyboard shortcuts
Press ? to see the full shortcut list. Key shortcuts:
- Click node → open sidebar with details
- Shift+Click a Level 1 (Hub) node → focus mode on the hub and its direct neighbours
- Shift+Click a Level 2+ node → silo focus: lights up every node sharing the clicked node's cluster, plus any broken-link nodes connected to a silo member, and dims everything else
- Shift+Click two nodes → shortest path between them
- Double-click title in sidebar → quick-rename the post
Sidebar node details
Click any node to see: Link Gravity score, level, cluster, inbound/outbound link counts, anchor text analysis, managed links connected to this page, and action buttons (Edit, View, Create Draft).
Export
Export dropdown offers: PNG image, SVG vector, CSV dataset (full, errors only, visible only), and CSV of managed links.
Toggle Silo Colors
The "🎨 Toggle Silo Colors" toolbar button swaps between two colouring schemes. Silo-coloured nodes get one hue per cluster so you can see topical silos at a glance; level-coloured nodes use the classic Hub / Pillar / Cluster / Long-tail / Orphan palette instead. Preference is persisted in the browser and applied on the first paint.
Hide Drafts
The "📝 Hide Drafts / Show Drafts" toolbar button hides every node whose post status is draft or scheduled. Useful for focusing on the live site structure when half the map is work-in-progress. Preference is persisted in the browser; the graph re-fits to the viewport after each toggle so the remaining published nodes fill the available space.
Category Map
The "📂 View Category Map" toolbar button switches the graph to a category hierarchy view. Every top-level category renders as its own standalone fan, roots are placed in a grid, descendants fan out in a half-circle below their parent, and physics is disabled so the fan coordinates are deterministic. The selector for picking which taxonomy to visualize only appears when the site has at least one custom (non-core) public taxonomy registered; otherwise the Category Map is always backed by WordPress's built-in category taxonomy.
Click Heatmap
The "Click Heatmap" toolbar button colors managed-link edges by click volume: green (high), yellow (medium), red (low), transparent (zero). Shows which affiliate links are converting.
Small-site layout
On sites with a clear Level 1 hub, the graph runs a post-stabilization rebalance: every direct child of the hub is assigned an equal angular wedge around it, and the child's entire subtree is rotated rigidly to match. Subtree shapes stay exactly as physics computed them, only their angular position moves, so Level 2 pillars with their own Level 3 children keep their organic layout while childless Level 2 siblings get the same "reserved slot" of empty space instead of collapsing into one side of the hub.
Filter Presets
Save and load named filter combinations. Stored in your browser (per user, per device). Useful for switching between "All content", "Only orphans", "Affiliate audit", etc.
Sample data
The Content Graph page has a "Generate Sample Data" tool at the bottom. It creates a realistic test content structure, a hub, several pillars, clusters with children, intentional broken links and orphans, plus a two-level category hierarchy so the Category Map has something to fan out from. Generations above 300 sample posts surface a confirmation dialog warning that very large runs can exhaust PHP memory on shared hosts and leave partial writes in the database. The maximum is 500.
Link Builder#
The Link Builder is a canvas-first internal-link editor. You plan a silo or cluster visually, adding nodes, dragging to reparent, drawing manual cross-links, then commit the plan as draft posts with the up/down/cross-link skeleton already wired in.
Open it from Gillish → Link Builder. The canvas IS the editor, not a preview. Quick-start templates (silo, hub-and-pillars, topical cluster) just drop a starting shape onto the canvas; from there you edit normally.
Canvas basics
- Click empty canvas → place a node at that position. The toolbar's "Add" group selects which role gets placed.
- Drag a node onto another node → reparent. The dropped node snaps into the widest open angular slot around the new parent (radius adjusts per role: 280 px for hubs, 160 px for pillars, 100 px for clusters, 80 px for long-tails, 120 px for free), with a short ease-out animation. The dragged node's role auto-demotes to fit: drop on a hub → it becomes a pillar; on a pillar → cluster; on a cluster → long-tail. If the title still has its default role name, it's renamed to the new role's default; custom titles are preserved (only colour + size change).
- Collision avoidance: every drop runs an iterative resolver that pushes the dropped node off any neighbour it would otherwise overlap with, so circles never sit on top of each other.
- Right-click a node → delete. Children are reparented one level up and inherit the deleted node's role (cluster → pillar, long-tail → cluster, etc.). Default-named nodes get the new role's default name; custom names are preserved.
- Connect mode (toolbar 🔗) → click two nodes to draw a manual cross-link. Esc to cancel.
- 📚 Add existing post → drop a real published post onto the canvas as a starting anchor. Existing posts render with a dashed border; commit doesn't touch them but the link skeleton can wire to and from them (a Pass 3 step appends new-child references to the existing parent's content).
Roles and the depth cap
The five roles (Hub / Pillar / Cluster / Long-tail / Free) match the Content Graph's level system. If you've renamed levels under Settings → Content Graph → Filtering & Depth, the Link Builder picks up your custom names automatically, toolbar buttons, default node titles, role select in node detail, plan summary stats, and empty-state CTAs all reflect the rename.
The depth cap (gn_max_depth) is advisory here, not a filter, over-cap nodes pulse a thick red border on the canvas and the Plan Summary shows a banner "⚠ N nodes exceed the {N}-level depth cap". Commit is not blocked.
Plan summary & commit
With nothing selected the sidepanel shows the Plan Summary: node counts per role, a required Batch label (you'll find the batch under this name in the audit list later), the post type to create drafts as, and the Validate plan / Create drafts buttons. Validate runs preflight (slug collisions, duplicate titles, Auto-Link keyword conflicts, batch cap); Create commits.
Use managed links for inter-node links (per-batch toggle)
Checking Use managed links for inter-node links in the Plan Summary tells the commit step to wrap every parent → child / child → parent / cross-link href in a tracked gillish_node redirect. One wrapper CPT per inserted node, all tagged with the same _gn_silo_builder_batch meta as the silo posts, so undoing the batch removes them automatically. The wrapper anchors get class="gn-direct" baked in so they don't double-light up the editor toolbar.
Off by default, opt in per silo when you want internal-link click data and accept the extra CPT rows.
Recent batches
Past commits are listed in the sidepanel's Recent batches section. Each row shows status, drift detection (when posts were deleted outside the batch), and these per-row actions:
- Resume: re-runs pass 2 for partial batches whose body still contains the placeholder marker (idempotent, only present when status is
partial). - Reuse: loads every post in the batch back onto the canvas as existing-post nodes (dashed border, never recreated on commit), reconstructed from the
_gn_silo_builder_role/_gn_silo_builder_parentmeta. Hub at origin, children fanned out radially. Add new clusters next to the existing nodes and commit a fresh batch, originals are untouched, only new nodes become drafts. The batch label is pre-filled as "{original} (extended)". - Export: downloads the batch as a JSON plan file (titles, slugs, roles, hierarchy, strips post IDs and permalinks). Hand the JSON to anyone, who can import it on their own site.
- Permanently delete: force-deletes every post AND removes the batch record, even if posts were cleaned up manually. The Trash action was removed in 2.63.1 because trashed batches stayed in the list with no useful follow-up actions; soft-undo via WordPress Trash is still available from the standard Posts list if you need it.
Export / Import plans
Above the Recent batches list, the Plan summary has ⬇ Export plan / ⬆ Import plan buttons. Export downloads the live canvas as a JSON file in the same format as the per-batch Export. Import accepts the same JSON shape, lays the nodes out radially per depth, replaces the current canvas, and pre-fills the batch label as "{original} (imported)".
The format is portable across sites, only structure is preserved (titles, slugs, roles, parent references, manual cross-links). Post IDs are dropped, so the importer always creates fresh drafts instead of trying to reference posts that don't exist on their site.
Keyboard shortcuts
- + / - zoom · ↑ ↓ ← → pan
- F toggle fullscreen (the small ⛶ button in the canvas top-right does the same)
- Ctrl/Cmd + Z undo · Ctrl/Cmd + Shift + Z / Ctrl/Cmd + Y redo
- Delete / Backspace remove the selected node or edge
- Esc cancel Connect mode
Module toggle
Settings → General → Modules has a "Link Builder" entry. Disabling hides the submenu and blocks the deep-link. Link Builder still depends on Content Graph being on (uses the same canvas primitives and AJAX endpoints), turning Content Graph off also hides Link Builder.
Link Management#
Create tracked redirect links at /node/your-slug (prefix configurable). Each link logs clicks, supports scheduling, and carries link tags like nofollow and sponsored.
Creating a link
Go to Manage Links → Add New Link. Enter a name, target URL, and optional slug. The plugin creates a redirect at your configured prefix. Set the category (affiliate, internal, social, other), redirect type (301/302/307), and link tags (nofollow, sponsored, ugc).
Inserting a link in the block editor
The Gillish Node button sits in the main block toolbar, the row that shows Bold and Italic. It appears on every block that has a text field, including blocks from other plugins, with nothing to configure. That includes the text fields inside structured blocks, for example the questions and answers of a Gillish Cairnstone FAQ block, or the steps of a How-to block. Click inside a link you made there and the button lights up, the same as in any other block, so you can edit or unlink it.
To insert a link: select some text, then click the Gillish Node button. A picker opens. Search by name, keyword, or URL. Pick a link, then choose how it should be stored:
- Synced: stores a small marker in the HTML (
<!--gn:ID-->). PHP builds the full link tag on every page load. If you later change the target URL, add nofollow, or update any link option, the live page reflects the change automatically, no re-editing the post needed. This is the recommended mode for most placements. - Direct: bakes the link tag into the post HTML at insert time (
<a href="/node/slug" class="gn-direct">). Link attributes are fixed at the moment of insertion. The URL still routes through the node redirect, so click tracking works, but attribute changes do not propagate until you re-insert. - Shortcode: wraps the selection in
[node id="42"]...[/node]. Useful in Classic Editor or wherever shortcodes are preferred.
The picker also has optional attribute overrides (new tab, nofollow, sponsored, parameters, and custom CSS classes) that apply to this placement only, without changing the link's global defaults.
To edit or remove a link you already inserted: click anywhere inside the linked text, then click the Gillish Node button again. The picker reopens showing the current link and an Unlink button. Click Unlink to remove the link and leave the anchor text in place. To swap the link, just pick a different one from the list.
Sub-IDs
Sub-IDs track individual link placements. Example: /node/amazon-camera/sidebar logs clicks separately from /node/amazon-camera/footer. Add sub-IDs in the link editor under the Sub-IDs section. Each sub-ID gets its own label and full URL preview.
Dynamic redirect rules
Rules redirect visitors to different URLs based on conditions. Available variables: country, device, browser, OS, weekday, time, date, total clicks, referer domain, language, random %, logged-in status. Rules evaluate top-down; first match wins; fallback is the main target URL.
Scheduling
Set a start date (link inactive before), expiry date (what happens after: show 404, redirect to target, or deactivate), and max clicks (auto-expire after N clicks, 0 = unlimited).
Link options
Per-link control over: open in new tab, nofollow, sponsored, ugc, noreferrer, parameter forwarding (none/all), CSS classes, tooltip text, and a preferred anchor text suggestion.
Convert from graph
Click any node in the content graph → the sidebar shows external links from that page. Click "Manage this link" to create a managed link from any external URL and replace it in the content.
A/B testing#
A/B testing splits one managed link between 2 to 5 destination URLs. Visitors who click the link are sent to one of the destinations, and the plugin records which variant each click went to, so you can see which destination pulls the most clicks. Everything is set up inside the link editor, so the public /node/ URL never changes, only where it routes.
Clicks are reach, not earnings. A click is counted before the visitor reaches the destination, so a click lead tells you which link gets used most, not which one makes the most money. For reach or branding, that is the answer you want. For sales, read your affiliate or shop report to see which destination actually earns.
Setting up a test
Open a managed link and expand the A/B Test (Destinations) section (it starts collapsed). Tick Active to enable the test, then give it a Test name for your own reference, for example "Amazon vs Walmart landing test". The test runs as soon as you save the link with Active on.
Variants, control, and weight
Each destination is a variant, labelled A to E. A test needs at least 2 variants and allows up to 5. Click + Add variant to add a row, or Remove to drop one. Each variant has three fields:
- Label: a name for your own reference, for example "Walmart".
- URL: where this variant sends the visitor.
- Weight: a number from 0 to 100 that sets how often this variant is picked, relative to the others. Two variants at 50 each split traffic evenly; 70 and 30 sends roughly seven in ten clicks to the first. A weight of 0 takes the variant out of rotation without deleting it.
One variant is the control. Tick Control (uses canonical URL) on the variant that should reuse the link's main Target URL. The control's URL field is locked and mirrors that Target URL, so the link's normal destination is always one of the variants under test. Only one variant can be the control at a time.
Rollout and stickiness
Rollout is the share of traffic that enters the test, from 1% to 100%. Start low, for example 10%, to warm a new test up safely; visitors outside the rollout get the normal Target URL. Bots never enter a test, so crawlers cannot skew the split.
Stickiness decides whether a returning visitor keeps the same variant:
- Cookie: the same visitor sees the same variant on later clicks. Use this when a consistent experience matters.
- None: a fresh pick on every click.
Reading the results
A link running a test shows an A/B badge in the Manage Links table. Open the link's detail view in Link Analytics to see the click count broken out per variant, so you can compare how each destination is doing.
The click-lead alert
Once a day the plugin checks every active test and asks one narrow question: does a single variant have a clear lead on clicks? When one does, you get a heads-up in two places:
- An email (rate-limited, so you are not flooded).
- A dismissible notice on the Gillish admin pages, naming the leading variant and how far ahead it is. Each admin dismisses it for themselves.
This is a soft "go and look" signal, not a verdict. It is not a statistical significance test, and the wording always says "ahead on clicks", never "winner". The same reminder applies: a click lead is reach, not earnings.
Auto-pause
Auto-pause is an optional, per-test setting, off by default. Tick Pause the other variants once one gets a clear click lead to turn it on. When the daily check finds one destination clearly ahead, the plugin sets every other variant's weight to 0, so all traffic flows to the leader. A paused variant shows an Auto-paused note with the date and the weight it had before. Nothing restarts on its own: to bring a variant back, tick Unpause on save on it and save the link. Because the signal is clicks, only turn auto-pause on if a click lead is the outcome you actually want.
Ending a test
When you are ready to stop, use End test: pick a winner from the dropdown and click End test with selected winner. The winner's URL becomes the link's canonical Target URL and the link stops splitting traffic. Pick the winner against your goal, not the dashboard: if you sell, choose the variant your affiliate or shop report shows earns the most, not just the one with the most clicks.
Auto-Link keywords#
Auto-Link scans your post content on the frontend and wraps matching keywords with managed-link URLs. The engine is backed by a SQL keyword index, with save-time conflict prevention, a per-keyword anchor-quality validator, distribution caps to keep pages from getting over-linked, and a sandbox preview that shows you exactly what the engine will do on any post before it hits the live site.
Adding keywords to a link
Open any managed link and scroll to Auto-Link Keywords. The section is a repeater, one row per keyword, each with its own match mode. Click Add keyword for each new row, or click Use link name to copy the link title into a fresh row. The last row can't be removed, it's just cleared, so there's always at least one input to type into.
Every row carries its own Match mode selector (see below), so you can mix strategies within a single link, one keyword set to First occurrence, another to Random, a third to None to reserve it without linking.
Match modes
- First occurrence: link only the first mention of the keyword in the post. Best for primary anchors.
- All occurrences: link every mention. Use sparingly; combine with the distribution caps below to avoid link spam.
- Last occurrence: link only the final mention. Useful for "learn more" style pointers near the end of an article.
- Random occurrence: pick one occurrence at random per render. Shuffles anchor placement across visits.
- None: keyword is registered in the index (and conflict-checked) but never linked. Use this to reserve a keyword for a link without auto-linking it, or to temporarily disable a single keyword without deleting the row.
Phrase matching with {gap:n}
A keyword can contain a gap token that lets the engine match phrases with up to n words between two anchors. Example: best {gap:3} camera matches "best cheap compact camera", "best underwater camera", "best mirrorless travel camera", etc., any phrase starting with "best" and ending with "camera" with up to three words in between. {gap:0} is a literal adjacency. The gap only skips word characters, punctuation breaks the match. Useful for capturing multiple variants of a product-category phrase without listing each one.
Pillar flag
A managed link can be flagged as a Pillar in the link editor. The flag decides who wins when two different keywords from two different links cover overlapping spans of text, for example, link A owns "best travel camera" and link B owns "travel camera", and both match the same sentence. Only one anchor can render, and the pillar gets it. The same resolver fires whenever a {gap:n} phrase swallows a shorter literal keyword or when two unrelated keywords share a word boundary.
Mechanically, the flag is a +15 priority bonus on top of the link's stored priority at candidate-evaluation time. The overlap resolver picks the highest effective priority, so a pillar link with priority 50 beats a non-pillar link with priority 60, and two pillars fall back to stored priority + keyword length. Use this to make sure cornerstone content always wins when spans collide. The flag is stored in the _gn_pillar_flag post meta, and the Sandbox Preview decision trace shows the pillar boost +15 label on every candidate that received it.
Anchor quality validator
Every keyword is run through an anchor-quality validator when you save. Rejected keywords are listed at the top of the page and removed from the repeater so you can see exactly which rows didn't survive. The validator rejects:
- Keywords shorter than 5 characters (unless the keyword matches the link name, brand names like "canva" are allowed via the link-name exception)
- Stop-word-only phrases (e.g. "the best of")
- Pure punctuation / whitespace
- Over-optimized anchors (see filter hooks below to tune)
Save-time conflict prevention
If you try to save a keyword that's already claimed by another link, the save succeeds but the keyword is stripped and a red conflict card appears with two buttons:
- Move to this link: transfers ownership from the original link to the one you're editing. After the move, the keyword is re-added to the repeater and the form re-submits automatically.
- Keep on owner: dismisses the card; the keyword stays with the original link.
Because conflicts are prevented at save time, there is no separate "Conflict Detector" scan to run, the index can't hold a conflicted state in the first place.
Per-link disable toggle
Each link has an Enable auto-linking for this link toggle in the editor. Turning it off leaves the keywords in place (so you don't lose them) but stops the engine from using this link as a link target for auto-linking. Use it to mothball a link that's broken or out of stock without deleting its keyword history.
Distribution caps
Configured under Settings → Auto-Link → Distribution. These apply per-post at render time:
- Max links per post: hard ceiling across all keywords
- Max per link per post: one keyword row can't dominate a page
- Max per paragraph: keeps any single paragraph from turning into a link stream
- Minimum character proximity: enforces a minimum distance between consecutive auto-links
- Intro guard: skip the first N characters of the post so the engine never drops a link inside the lede
DOM walker (PHP 8.4 recommended)
The Use DOM walker toggle in Settings switches the engine from the legacy regex-based plan/commit model to a DOM-aware walker built on native Dom\HTMLDocument (PHP 8.4). The DOM walker is more accurate in edge cases, it never touches text inside <script>, <style>, <code>, or existing anchors, and it won't corrupt HTML entities. On PHP 8.2–8.3 the walker falls back to DOMDocument. Both engines use a stripos pre-check so posts without any candidate keyword are skipped entirely.
Sandbox Preview
On the Auto-Link page, the Sandbox Preview tab lets you pick any post, run the engine against it in dry-run mode, and see exactly what would change, with a decision trace next to each keyword explaining why it was linked, skipped, or rejected. Use this before shipping a new set of keywords on a post you care about.
Link Health dashboard
The Auto-Link page carries a Link Health dashboard with five tiles:
- Healthy: destination resolves, link active, has keywords
- Broken: destination returns 4xx/5xx or a connection error
- Redirect chain: destination redirects more than once
- Unchecked: never checked or check is stale
- Top destinations: most-used auto-link targets, to spot monoculture
Health checks run automatically on a WP-Cron schedule and when you click Run health check now. Broken destinations are filtered out of the auto-link engine at render time, so a broken link never ships to readers, it just stops linking until you fix it. Only links with at least one keyword are checked (there's no value in checking a link that auto-link doesn't use).
Auto-Link list table
The list shows every link with keywords, plus a health badge, a match-mode badge, and an inline auto-link toggle. Bulk actions cover enable, disable, and delete. The table reuses WP_List_Table so screen-options column controls work as expected.
Bulk import
Paste lines of the form:
keyword1, keyword2, best {gap:2} camera > link-slug
One line per link. The separator is a plain > (greater-than). Keywords are the comma-separated part on the left; the link slug is on the right. Each keyword runs through the same anchor validator and conflict check as the repeater UI, so malformed lines are rejected with per-line feedback.
Excluding a post
Three ways to flip a post in or out of the auto-link engine, they all set the same on/off marker on the post (_gn_disable_autolink), so it doesn't matter which one you use:
- From the posts list (Posts / Pages / any public CPT): every edit.php screen gets an Auto-Link column with a green Active / red Excluded badge. Click the badge to toggle the post without leaving the list. The toggle runs through AJAX so there's no page reload, and the label/color updates in place. This is the fastest way to sweep through a batch of posts.
- From the post editor: tick Disable auto-linking on this post in the Auto-Link sidebar box (Classic Editor and Gutenberg both show it as a standard sidebar box).
- From the Auto-Link admin page: the Excluded Posts panel lists every excluded post and offers a one-click re-enable for each row.
Programmatically: set post meta _gn_disable_autolink to yes.
Click tracking through sub-IDs
Auto-link anchors can be wired through sub-IDs so you can see which auto-linked page is driving clicks. Each auto-link rendered from the engine carries a sub-ID derived from the source post, so the Analytics page will break the clicks out under the managed link's detail view.
Global settings
In Settings → Auto-Link: global on/off, DOM walker toggle, distribution caps, post types to process, skip logged-in users, and the health-check cadence.
Filter hooks (developers)
gillish_node_autolink_candidates, filter the final candidate set before renderinggillish_node_autolink_anchor_attrs, modify the<a>attributes on a rendered auto-linkgillish_node_autolink_should_process, short-circuit processing for a given postgillish_node_autolink_skip_element, exclude additional HTML tags from the DOM walkergillish_node_autolink_anchor_validator, plug custom rules into the anchor-quality check
Link editor: default open sections
The editor remembers which sections you had open when you clicked Update, and re-opens them after the page reloads. If you want a specific set of sections open by default when you arrive from Manage Links, configure them under Settings → Link Management → Default open sections. Focus mode (arriving from the Auto-Link page with ?expand=…) still takes precedence.
URL Shorteners#
Connect an external shortening service to generate short URLs for your managed links. The short URL is stored alongside the link and displayed in the editor and list table.
Supported providers
- Bit.ly: requires a personal access token from
app.bitly.com - Short.io: requires an API key and your custom domain
- Rebrandly: requires an API key; optional custom domain
- TinyURL: works without credentials (rate-limited); optional token for higher limits
- YOURLS: self-hosted shortener; requires your API URL and signature token
Setup
- Go to Settings → Shorteners tab
- Select your provider from the dropdown
- Enter the required API credentials
- Save, the "Shorten URL" button now appears in the link editor
Extending
Developers can add custom providers via the gillish_node_register_shorteners filter. Implement the ShortenerProvider interface (id, label, shorten, settings_fields, is_configured).
Link Analytics#
The analytics dashboard shows click activity across all your managed links.
Dashboard overview
- Summary cards: total clicks, unique links clicked, top country
- Click trend chart: line chart showing daily clicks (Chart.js)
- Top links table: most-clicked links with click counts
- Geographic breakdown: clicks per country code
- Top referers: where clicks come from (Google, Facebook, direct, etc.)
Date range
Switch between 7d, 30d, 90d, 1y, or All time using the buttons above the dashboard.
Per-link detail
Click any link name in the top links table to see its individual click trend and sub-ID breakdown.
Click logging
Every redirect logs: link ID, sub-ID, timestamp, referer, user agent (hashed), IP (one-way hashed), and country code. Logging is asynchronous (scheduled event) to avoid slowing down the redirect. Old click records are pruned based on the retention setting and aggregated into summary counts.
GeoIP
Country code resolution uses the GeoIP module (if enabled). Supports MaxMind GeoLite2, IP2Location Lite, or Cloudflare's CF-IPCountry header. Configure in Settings → General → enable the GeoIP module.
Search Performance
Separate page (Gillish → Search Performance) backed by the Google Integrations module. Shows aggregated per-page Search Console data (clicks, impressions, CTR, position) and a Top Queries table with click + impression + CTR + position columns. Same data layer as Rank Tracker, but presented as a current-state snapshot rather than a time series.
To restrict every GSC fetch to a single country (so a Norwegian site doesn't get average-position diluted by US visibility), pick a country under Settings → Google Integrations → Filter by country. Default is "All countries (default)" which aggregates globally, match GSC's default behaviour. Re-run "Run sync now" after changing.
Rank Tracker#
Rank Tracker turns the daily Search Console per-query sync into a historical time series so you can see which keywords moved, in which direction, and by how much. Everything comes from the data Google Integrations already pulls every day, no third-party SERP API, no per-query billing, and nothing for you to type in manually.
Enabling Rank Tracker
Rank Tracker is an opt-in module. Go to Settings → Modules and switch it on. The module requires Google Integrations to be enabled and fully configured, it has nothing to track without daily GSC data flowing in. Once enabled, a new Gillish → Rank Tracker submenu appears.
Where the keywords come from
Every keyword tracked by Rank Tracker was auto-discovered by Google Search Console. You cannot add keywords manually, that would require point-in-time SERP scraping, which is expensive and not what this module is for. Once a page starts ranking for a query, GSC surfaces it in the daily sync, Rank Tracker captures a snapshot, and the historical series begins from that day forward. A keyword that drops out of GSC's reporting window disappears from the tracked set automatically.
This means Day 1 of Rank Tracker is always blank. There is no way to backfill historical ranks, GSC doesn't expose that data as a single request and the API budget to synthesise it would be absurd. Come back after a week for meaningful 7-day deltas and after a month for trend lines.
The Rank Tracker dashboard
Four overview tiles at the top: pinned keywords, total tracked pairs, days of history accumulated so far, and the last snapshot timestamp. Below that, three panels:
- Pinned keywords: every (page, query) pair you've explicitly chosen to follow, sorted by recent click volume. Each row shows the current position, 7-day change, 30-day change, clicks and impressions over the last 3 days, and a mini Chart.js sparkline of the last 30 days. Position 1 sits at the top of each sparkline (y-axis inverted) so "the line going up" means "ranks improving".
- Today's movers: unpinned (page, query) pairs whose position has moved by more than the configured threshold (default ±3 positions) over the last 7 days. Sorted by biggest absolute change first. Each row has a Pin button to promote it into the Pinned panel for ongoing tracking.
- Keyword search: type any substring and see every tracked page that ranks for a matching query. AJAX-driven, 250 ms debounce, returns latest snapshot only. Useful for ad-hoc lookups when you remember a query but not which page it belongs to.
What pinning does
Pinning is a filter flag, not a way to add new keywords. You can only pin a query that GSC has already surfaced for one of your pages, if the keyword isn't in the history, there's nothing to pin. Pinning changes three things:
- The query appears in the Pinned panel at the top of the dashboard for easy filtering.
- Retention is longer, unpinned rows get pruned after 90 days by default, pinned rows stay forever (configurable).
- The query shows with a 📌 marker and amber highlight in the post editor's Google Performance box, plus an inline sparkline.
Unpinning never deletes history. The row flips its is_pinned flag back, moves to the shorter retention window, and stays in the dashboard's Today's Movers panel if it's still moving enough.
Post editor delta chips
Every post with GSC data now shows a third line per query inside the Google Performance box: 7-day and 30-day position deltas, colour-coded green for improvements and red for regressions. Pinned queries get an amber border highlight and an inline 30-day sparkline next to the query text. Rank Tracker has to be enabled for this block to appear, otherwise the box stays in its classic layout.
Content Graph rank-mover overlay
A new Rank Mover element flags any page whose click-weighted 7-day rank delta exceeds a configurable threshold. Mover pages get an amber border in the Content Graph and a stat entry in the legend. A separate Colour by 7-day rank movement overlay toggle (in the Advanced SEO Insights panel) recolours every node on a green-to-red gradient by aggregate movement, so you can see at a glance which silos are improving and which are slipping.
Click any node with rank data to see a Rank Tracker · 7-day movement block in the sidebar with the aggregate verdict, the single biggest mover query driving it, and the total number of tracked queries contributing to the score.
Alerts
When a keyword crosses the alert threshold (default ±5 positions, configurable via the gillish_node_rank_tracker_alert_threshold option), Rank Tracker fans the event out to four parallel delivery surfaces, each one opt-in by its own gate so you can enable exactly what you want.
- Admin notice: a dismissible info notice on the next Gillish admin page load showing the top 3 movers plus a link to the full dashboard. Always on when Rank Tracker is enabled. Dismissed per-user so co-admins don't shadow each other.
- Email digest: a plain-text email via
wp_maillisting every mover with arrow, query, direction, delta, and permalink. Opt-in by settinggillish_node_rank_tracker_alerts_emailto a comma-separated recipient list. Rate-limited to one digest per 24 hours per site. - Webhooks: if the Webhooks module is also enabled, every mover fires a
rank_mover_detectedpayload onto the configured endpoint. Zapier, Make, n8n, and Slack (via Incoming Webhooks) all work out of the box, this is how you get Slack delivery without a dedicated Slack dispatcher. - WordPress dashboard widget: a Gillish · Rank Movers (7 days) widget on the standard WP Dashboard showing the top 5 biggest movers with a colour-coded 7-day change column. Reads fresh data on every dashboard load, no caching layer to invalidate.
Data retention
Two retention windows keep the history table bounded:
- Unpinned rows: default 90 days, configurable via
gillish_node_rank_tracker_retention_unpinned_days. A (page, query) pair that drops out of GSC's reporting window stops getting new snapshots but keeps its existing rows until the cutoff. - Pinned rows: default forever (
0means no pruning), configurable viagillish_node_rank_tracker_retention_pinned_days. Set this to a number of days if you want to cap how far back pinned history goes.
Retention prune runs on every SnapshotJob pass immediately after the new rows are written, so the table never grows unbounded between cron ticks.
Why this isn't Ahrefs
Ahrefs, Semrush, and SerpAPI all have vastly more data than Gillish Node's passive GSC tracking will ever have. What they don't have is the link graph of your own site, the auto-link engine that can actually act on a ranking insight, and the integration with the page editor where the rewrite happens. Rank Tracker is deliberately lightweight so it runs on every host without API credits, if you need point-in-time SERP accuracy on specific keywords, pair it with a dedicated rank-tracking service and use both. They answer different questions.
Webhooks: traffic-aware health alerts#
Gillish Node ships a unified webhook stream (gillish_node_events) that covers two event types under a single endpoint URL. The plugin POSTs an enriched JSON payload to an endpoint of your choice, a Zapier Catch Hook, a Make webhook, an n8n Webhook node, or any URL that accepts JSON. Every entry in the payload carries a type field so a single flow can branch on event type in one filter step.
Event types in v1:
managed_link_status_change, a managed link's destination URL flipped health classification (healthy ↔ broken ↔ redirect_chain ↔ unreachable). Enriched with click volume and Search Console traffic.content_broken_link, the Content Graph detected an internal<a href>in a published post that no longer resolves to any active post. Fires on newly-detected brokens only; known-broken links don't re-fire until they heal and break again.
Setup
- Go to Settings → General and enable the Webhooks module.
- Go to Settings → Link Management and scroll to the Webhooks section at the bottom.
- Paste your endpoint URL into the Webhook URL field. Leaving it empty silently disables delivery without turning the module off.
- (Optional) Adjust the rate-limit interval. Default is 3600 seconds (1 hour); valid range is 60–86400.
- Save. Delivery starts on the next health classification change.
When each event fires
managed_link_status_change fires from Link Health checks, either the daily WP-Cron run or the manual "Run health check now" button on the Auto-Link page. Every published or draft managed link gets its target URL probed, whether or not it has auto-link keywords. When a probe result's classification differs from the previous probe's, one event is queued for that link.
Classifications are derived from the HTTP response code:
- healthy: 200–299
- redirect_chain: 300–399 (the checker follows up to 3 redirects; a stored 3xx means the chain exceeded the hop limit)
- broken: 400 and above
- unreachable: network error, DNS failure, timeout, or first-ever check on a freshly created link
Raw-code changes that stay inside the same classification (for example 404 → 410 or 200 → 204) do not re-fire. A recovered link (unreachable → healthy) fires an event too, so your flow can build a "link is back" notification.
content_broken_link fires from the Content Graph element pipeline. When the graph is built, LinkGraphService DOM-parses every published post and detects internal <a href> links that don't resolve to any active post. Newly-broken links, ones that weren't in the previous graph build's broken set, each fire one event. The known-broken set is persisted in gillish_node_webhook_known_broken_content_links so a reported link stays quiet until it heals and breaks again.
Important timing note: Content Graph builds are triggered on demand, when someone visits the Content Graph admin page, or when the graph cache expires and the next visit rebuilds it. There is no daily cron that forces a graph build just to scan for broken links. On a quiet admin site, days can pass between detections. If you need guaranteed daily scanning, visit the Content Graph page once a day (it takes a few seconds) or bookmark it, any manual load triggers the scan.
Scope limit: this event type only covers internal broken links (relative URLs or absolute URLs pointing at the same site whose target doesn't resolve to a post). External broken links (third-party URLs returning HTTP 404) are not scanned in v1, that's a separate content-scanner feature tracked in the gap plan.
Payload shape
Every webhook POST contains an envelope and a links array. Entries of different types share the array, the type field on each entry tells the receiver how to interpret the remaining keys. Multiple events that land inside a single rate-limit window are aggregated into one POST.
{
"event": "gillish_node_events",
"timestamp": 1776200071,
"site_url": "https://your-site.com",
"links": [
{
"type": "managed_link_status_change",
"link_id": 5355,
"slug": "amazon",
"title": "Amazon",
"destination_url": "https://www.amazon.com/",
"classification_old": "healthy",
"classification_new": "broken",
"http_status_old": 200,
"http_status_new": 404,
"node_clicks_30d": 145,
"source_post_count": 7,
"source_post_gsc_clicks_30d": 420,
"manage_url": "https://your-site.com/wp-admin/admin.php?page=gillish-links&action=edit&link_id=5355"
},
{
"type": "content_broken_link",
"source_post_id": 314,
"source_post_title": "Best compact cameras of 2026",
"source_post_url": "https://your-site.com/best-compact-cameras-2026/",
"target_url": "/old-camera-roundup/",
"detected_at": 1776200068,
"manage_url": "https://your-site.com/wp-admin/post.php?post=314&action=edit"
}
]
}
Envelope fields
event, always the stringgillish_node_events(generic envelope; branch onlinks[].typeto route)timestamp, unix timestamp of the dispatch moment, not the probe/detection momentsite_url, the result of WordPress'shome_url(), so you can route by source site if you run severallinks[], array of mixed-type entries, each with its owntypefield
Fields for managed_link_status_change
type, alwaysmanaged_link_status_changelink_id, the managed link's post ID, stable and safe to key on in your flowslug, the link's URL slug (for exampleamazonfor/node/amazon)title, the link's display namedestination_url, the target URL the checker probedclassification_old/classification_new, one ofhealthy/broken/redirect_chain/unreachablehttp_status_old/http_status_new, raw HTTP codes, included for fine-grained filtering (for example "only alert on 5xx")node_clicks_30d, clicks recorded against the managed link in the last 30 days, from the plugin's own click log. Authoritative for "how often was this link actually used?"source_post_count, number of distinct published posts that currently render an auto-link anchor pointing at this link (from theautolink_relationstable)source_post_gsc_clicks_30d, 30-day sum of Google Search Console clicks across those source posts.nullwhen Google Integrations is disabled, when there are no source posts, or when no GSC data has been synced for those URLs. A real0would mean "data exists but zero clicks",nullmeans "we don't know."manage_url, deep-link back into the Gillish Node editor for the affected link, ready to drop into an alert message
Fields for content_broken_link
type, alwayscontent_broken_linksource_post_id, ID of the published post that contains the broken anchorsource_post_title, title of the source post for human-readable alertssource_post_url, permalink of the source posttarget_url, the broken URL path as it appears in the source post's HTML (e.g./old-slug/)detected_at, unix timestamp of the graph build that detected this breakmanage_url, deep-link back into the WordPress editor for the source post so you can fix or remove the anchor
Rate limiting and batching
Events queue in a plain WordPress option (not a transient, so cache plugins can't evict the queue). The queue drains at most once per Minimum seconds between dispatches (default 3600). During a multi-link outage, every classification change appends to the queue until the interval expires, then all pending events ship as one aggregated POST, so your phone doesn't ring 200 times when an entire destination domain goes down.
If the queue exceeds 500 buffered entries, a pathological outage involving hundreds of links, the module force-drains regardless of the rate limit to prevent the option from growing unbounded.
On dispatch failure (non-2xx response or a network error against your receiver), the queue is left in place and the next event triggers another attempt. Failures are logged to the PHP error log; enable WP_DEBUG_LOG in wp-config.php to see them.
Traffic-aware routing: the point of all this
The managed_link_status_change payload ships the node_clicks_30d and source_post_gsc_clicks_30d fields specifically so your flow can filter by real impact in one step. A typical Zapier setup:
- Trigger: Catch Hook → receive webhook
- Filter:
links[0].type = managed_link_status_changeANDclassification_new = broken - Path A (high impact):
node_clicks_30d > 100ORsource_post_gsc_clicks_30d > 500→ SMS via Twilio - Path B (low impact): otherwise → Slack message to #link-alerts
- Path C (internal housekeeping): separate filter branch for
links[0].type = content_broken_link→ post to #content-maintenance Slack channel withsource_post_titleandtarget_url
The equivalent setup with other link-management plugins requires a second integration call back into WordPress to fetch click counts, because their webhooks ship only {link_id, url, status}.
Testing your endpoint
The fastest way to confirm delivery works:
- Create a free endpoint at webhook.site and copy the unique URL it gives you.
- Paste it into the Webhook URL field and save.
- Create a managed link pointing at
https://example.com/does-not-exist-404and add at least one auto-link keyword so the health checker picks it up. - Open the Auto-Link page and click Run health check now.
- webhook.site shows the incoming POST within a few seconds. Expand the body to inspect the payload.
A first-ever check on a newly-created link fires unreachable → healthy if the destination returns 2xx on its first probe. That transition is intentional, it lets your flow distinguish "new link validated OK" from "new link already broken."
Kill switches
Two independent ways to stop delivery cold:
- Module toggle: Settings → General → Webhooks. Turning this off unhooks the listener entirely. The event queue is not cleared, so re-enabling resumes from where delivery stopped.
- Empty URL: Settings → Link Management → Webhook URL. Leaving the field blank short-circuits both queueing and dispatch without touching the module gate. Useful for temporarily pausing delivery to one receiver without unhooking the whole module.
Developer integration
The webhook module is one consumer of two general-purpose action hooks. Third-party code can subscribe to either:
// Managed-link classification change (from LinkHealthChecker).
add_action(
'gillish_node_link_health_status_changed',
function ( $link_id, $old_class, $new_class, $old_code, $new_code ) {
// Custom logging, Slack webhook, PagerDuty trigger, etc.
},
10,
5
);
// Newly-detected internal broken link (from Content Graph build).
add_action(
'gillish_node_content_broken_link',
function ( $source_post_id, $target_url ) {
// Custom logging, automatic anchor rewrite, etc.
},
10,
2
);
Both hooks fire before the Webhooks module's listener, so custom subscribers can short-circuit or enrich the event flow using standard WordPress action priority.
Settings & Health#
The Settings page has 7 tabs:
- General: enable/disable modules, presentation mode
- Content Graph: sitemap URL, level names, map height, node sizes, visibility, algorithm parameters, physics preset, external path prefixes
- Link Management: URL prefix, separator, default redirect type, click data retention
- Auto-Link: global toggle, max per post, post types, skip logged-in
- Shorteners: provider selection, API credentials
- SEO Detection: element toggles, detection thresholds, edge colors, panel/legend visibility
- Health Check: server environment, cache status, content overview, link health, target URL verification
Health Check
Shows PHP version, memory limit, execution time, WordPress version, plugin version, graph cache status, and content counts. Warnings appear if any value is too low. The link health section shows managed link totals, expired links, and zero-click links. "Verify All Target URLs" checks each managed link's destination for 404 errors.
Module system
Gillish Node is modular. Each feature (link-core, link-manager, analytics, shorteners, auto-link, geoip) is an independent module that can be enabled or disabled in the General tab. Disabling a module removes its menu items and stops all its work entirely.