gtmesh ui
Open the mesh’s local, read-only control room. gtmesh ui starts a small loopback dev-server
(bound to 127.0.0.1 — no external network) and opens your browser on the Dashboard: how the site is
doing in Google — visits, rankings, trends — without opening Search Console. One tab over, Topics
lists every topic you’re fighting for and how each is doing, in plain language; Pages does the same
per page — its status, whether Google has indexed it, and what it earns; Links shows the external
authority arriving into the mesh — who links to the site, and where those links land. The deeper
surfaces — Data, Graph — render the committed graph artifacts (graph/nodes.jsonl,
graph/edges.tsv) for when you want the structure itself.
gtmesh ui [--view dashboard|topics|pages|links|db|graph|manifest] [--port <n>] [--open|--no-open] [--env <env>]It is read-only, in the mold of doctor — it never writes committed state — and it is not part of
the deterministic pipeline: it’s a dev tool for looking at your working mesh while you iterate. It runs until
you press Ctrl+C; there is no committed output (scratch lives under the git-ignored
.gtmesh/ui/). As you apply/seal in another terminal — or pull a fresh performance snapshot — the view
live-reloads.
The six tabs
| Tab | Question it answers | --view |
|---|---|---|
| Dashboard (landing) | How is the site doing in Google? | dashboard |
| Topics | What are we fighting for, and is it working? | topics |
| Pages | Is every page live, indexed, and earning? | pages |
| Links | Who links to the site, and where does that authority land? | links |
| Data | What exactly is in the graph, record by record? | db |
| Graph | What does the whole mesh look like as a structure? | graph |
Pages replaces the earlier Site tab — the website-as-a-hierarchy view lives on inside it as the
Tree toggle, and old links keep working:
--view manifest and #manifest permalinks open Pages with the Tree toggle active.
Dashboard, Topics, and Pages read the latest performance snapshot per provider — the committed
files pull performance --source gsc (and
each other configured performance provider) writes under data/raw/performance/<provider>/ — plus
the AI-visibility snapshot when one exists, joined
against the committed graph at view time. It’s the same join
topic performance prints, rendered visually; the snapshot is a
read-time overlay next to the graph, and plan/apply keep working from demand alone, exactly as before.
Dashboard — how is the site doing?
The header names the data you’re looking at: the snapshot’s report window and its age (“Search data Jul 16 – Aug 12 · pulled 2 days ago”) plus the refresh command. Below it:
- Four stat tiles — Visits · Times shown · Average ranking · Click rate (with the “typical for #N” context) — each showing the current window against the previous one, with a delta chip and a daily trend line. When more than one search engine reports, the two event tiles (visits, times shown) sum across sources with a per-engine split line under each (“Google 1,204 · Bing 87”), while ranking and click-rate context stay in one engine’s labeled frame — see Sources.
- Where to focus — the four health states as buckets, with counts and topic chips that link straight into the Topics tab.
- Biggest moves — the top ranking gainers and losers by topic since the previous window.
- Coverage — topics fought for, pages live and in progress, topics with data — and Outside the mesh: the share of the property’s search traffic landing on URLs the mesh doesn’t manage.
- Indexing — how many published pages Google has indexed, the not-indexed reasons in plain words (with a note for pages too new to judge), and any sitemap errors when the property reports them. Click through to the Pages tab for the per-page detail.
- Authority — the site’s backlink profile at a glance: Site strength (Ahrefs Domain Rating)
with its trend, live referring-domain and backlink totals, and the top referring domains — linking
through to the Links tab. Its empty state points at
gtmesh pull links. - AI visibility — how often public AI answers (ChatGPT, Google AI Overviews, Gemini, Perplexity, Copilot) cite the site as a source: per-platform counts of citing answers and cited pages, with an empty state pointing at the pull. See AI visibility for what the numbers mean.
Sources — which engine you’re looking at
Once more than one search engine has a performance snapshot (say Bing joins Google), each
performance page grows a source filter below the navigation — it lives on the pages it applies
to, never in the global header (Data and Graph don’t read performance). The Dashboard offers
All sources · Google · Bing; Topics and Pages offer just Google · Bing, because everything
on them is a per-engine judgment with no blended view. The choice sticks as you switch tabs and
rides the URL (#topics?src=bing) so a scoped view is shareable. The refresh command becomes
gtmesh pull performance, which pulls every configured provider in one go.
The rule underneath is aggregate events, never judgments:
- Event counts add up. A visit is a visit whichever engine sent it, so “All sources” sums visits and times shown across engines (and recomputes the click rate from the sums). Each tile’s sparkline draws one line per engine in that engine’s color, and the split line beneath (”● Google 1,204 · ● Bing 87”) doubles as the legend.
- Judgments don’t. A #3 in Google is not a #3 in Bing, and each engine’s click-rate baseline is its own curve — so rankings, CTR-vs-typical, and every health verdict always read within one engine’s frame, never blended. In “All sources” those surfaces show Google (the default judgment frame), clearly labeled; scope the filter to another engine to see the same scoreboard in its frame.
Filter-and-compare is the diagnostic this exists for: a topic slipping in one engine points at that engine’s ranking systems; slipping in both points at the content. With a single connected source there is no filter and everything reads exactly as before. (Indexing detail is Google URL-Inspection data whichever source is active, and the AI-visibility card has its own providers — the source filter doesn’t touch it.)
Topics — the scoreboard
A ranked table, not a graph. Each row: the topic (with its main term) · a health chip · Opportunity as a relative bar · searches/mo · ranking + trend · visits · referring domains (how many distinct sites link to the cluster, from the latest backlink snapshot — an em-dash until one exists) · a short next step. Every numeric column sorts. Filter by health state (chips with counts) or free text; the default sort is the CLI’s own order — worst health first, then opportunity — so the top of the table is where effort goes.
Click a row and it expands into the topic’s full story:
- What’s happening — the engine’s evidence, in page-1/page-2 language rather than raw deltas.
- What to do — the fix, with the exact command in a copyable block.
- External authority — the cluster’s referring domains and backlinks, and the apex’s share (“6 of 8 referring domains point at the topic’s home page”) — the “content problem vs link problem” read: a topic that’s worth a push with strong content and no referring domains needs links, not a rewrite. The sums count a domain once per page it links to.
- The cluster — the hub/pillar and its member pages, each with its own ranking / visits / trend (and its referring-domain count once a backlink snapshot exists), plus a one-line reminder of how members flow authority up to the apex.
A “How to read this” glossary sits at the foot of the page, and rows are deep-linkable
(#topics?sel=<value>), so a topic’s story has a URL you can share.
The four health states
Every topic wears one of four chips. They are plain-language groupings of the engine’s verdicts — the same
topic performance prints — and each chip’s tooltip names
the underlying signal and threshold, so nothing is hidden behind the translation:
| Health state | Engine verdict(s) underneath | Reads as | What to do |
|---|---|---|---|
| Losing ground (red) | decaying | rankings dropped vs the previous window | act here first — refresh and improve the slipping pages |
| Worth a push (amber) | apex-unbuilt (“Home page not live”) · striking-distance (“Within reach”) · ctr-gap (“Ranking, not clicked”) · head-unranked (“Main term not ranking”) | the opportunity pile — the chip text names which kind | publish the topic’s home · strengthen the apex · rework titles/meta · point the apex at the main term |
| Healthy (green) | served | ranking with a healthy click rate | leave it alone |
| Quiet (gray) | no-data (“No search data yet”) · unbuilt (“Not built yet” — a hollow chip) | too new, or not started | give it a window · build it |
The verdicts, thresholds, and fix pointers all come from topic performance — see
its reference entry for exactly when each fires.
How to read the numbers
The same translation covers the metrics: every number gets a plain name, and its ⓘ carries the definition.
| On screen | Meaning | Engine field |
|---|---|---|
| Visits from Google (from Bing / from search, per the active source) | times someone clicked through from a search result | clicks |
| Times shown | how often a page appeared in results, clicked or not | impressions |
Average ranking (#12.4) | the average spot in results across every search you were shown for, weighted by times shown — lower is better; #1–10 is page 1. Starting to appear for new, lower-ranked searches can pull it down even as visibility grows, so read it next to Times shown | position |
| Click rate (vs “typical for #N”) | the share of viewers who clicked, against your site’s own average at that ranking | ctr / expected_ctr |
| Trend arrow ▲/▼ (“up/down N places”) | ranking movement vs the previous window, as direction words | position_delta |
| Opportunity (relative bar) | monthly searches × click value — the same score the CLI ranks by | opportunity |
| Main term | the search the topic exists to win | head_keyword |
Pages — every page, its status, and what it earns
Where Topics is the scoreboard per topic, Pages is the same story per page — a filterable list of every page in the mesh, not a tree to click through. Each row:
- the page — slug and title;
- its topic — the topic it belongs to, with a HUB / PILLAR badge when the page is that topic’s apex;
- its lifecycle status — where the page sits in the build (
writing,review,published, …); - an indexing badge — whether Google has the page (see the states below);
- its ranking + trend and visits — the same plain-language numbers as Topics;
- its referring domains — distinct sites linking to the page, from the latest backlink snapshot (an em-dash until one exists). Sortable, like every numeric column.
Filter by section, lifecycle status, indexing state, or free text. Click a row and a drawer opens with the page’s full story (the same pattern as a Topics row):
- What people searched to find it — the page’s top queries in the current window, each with its visits, times shown, and ranking.
- External links — the page’s backlinks, referring domains, and page strength (Ahrefs URL Rating) when the link index serves them.
- Indexing detail — the state in plain language, when Google last crawled the page, and whether Google agreed with your canonical.
- Its topic — the membership, linking straight to the topic’s story on the Topics tab.
- Jump-offs: the live page ↗ (published pages only) and the authored source
(
content/<id>/index.yaml) via the Data record.
Rows are deep-linkable (#pages?sel=<id>), and the cluster rows in a Topics drawer link here — so
“this topic is slipping” drills straight down to “which page, and why”.
The Tree toggle. The website-as-a-hierarchy view (home → section indexes → leaf pages, read from
the compiled site.manifest.json, with each page’s internal-link relations and planned-vs-published
up-link divergence) is one click away behind the Tree toggle — the list answers “how is this page
doing?”, the tree answers “where does it sit?”. --view manifest and #manifest permalinks open
Pages with the Tree active.
The indexing states
A published page Google hasn’t indexed earns nothing — this is the health axis the badge surfaces. The states are plain-language readings of the URL-Inspection data the snapshot carries:
| Badge | Reads as | What it usually means |
|---|---|---|
| Indexed | Google has it | nothing to do |
| Crawled, not indexed | Google looked and passed | usually a content-quality or duplication signal — improve the page |
| Discovered, not crawled | Google knows the URL but hasn’t visited | internal links and a sitemap entry help it get there |
| Canonical mismatch | Google chose a different canonical | usually duplication — two pages competing for the same search |
| Not live yet | the page isn’t published | nothing to inspect until it ships |
The data comes from the same
pull performance --source gsc pull — it now
also sweeps every published page’s URL through Google’s URL Inspection API (plus your sitemap status)
and commits the result beside the analytics snapshot. The sweep needs site.base_url set (the command
warns with the fix if it’s missing) and inspects published pages only; --no-index skips it. The
operator skill carries the matching playbook — what to actually do about each state.
AI visibility
People increasingly ask ChatGPT instead of Google — this is the surface that tells you whether those
answers cite you. gtmesh pull ai fans out to every configured AI provider (today: Ahrefs,
reusing the token you already have; one flat-cost request per pull) and snapshots, per platform —
ChatGPT, Google AI Overviews, Gemini, Perplexity, Copilot — how many public AI answers cite the
site and how many distinct pages they cite. It needs site.base_url set — the command’s error
carries the fix — and commits each provider’s snapshot to data/raw/ai/<provider>/, beside the
Search Console snapshot.
- A citation means an AI answer used a page of yours as a source — the AI-search equivalent of ranking. The Dashboard card shows each platform’s count of citing answers and cited pages.
- Providers are never merged — each samples AI answers its own way — so when more than one reports (Bing joins with the provider phase), the card names each source.
- Zero citations is a normal starting state for a young site — AI answers cite established sources, and citations follow the authority you’re already building. The empty states say so rather than showing a wall of zeros; keep building, and let answer-quality (the AEO gate) do its work.
Like the Search Console data, this is an observation channel: the snapshot is read at view
time, and plan/apply keep working from demand alone — what AI cites never silently changes what
the engine builds.
Links — who links to the site
The mesh controls internal authority flow — spokes route weight up to their hub. Backlinks are
the external authority arriving into that structure, and the Links tab is where you watch it:
gtmesh pull links snapshots the site’s backlink profile from each configured link index
(Ahrefs primary; Bing’s free per-page counts as a cross-check) into data/raw/links/<provider>/,
and the tab joins the latest snapshot against the graph at view time.
- The header carries the site-level read: Site strength (Ahrefs Domain Rating, 0–100) with its trend line, live referring-domain and backlink totals, the snapshot’s age, and the refresh command.
- The referring-domains table is the who-links-to-us list, laid out like Topics: a full-width
sortable table with each linking domain’s own site strength, how many links it points at you,
and when it was first seen. A free-text filter narrows it, and clicking a row opens the domain’s
drawer (the selection rides the permalink,
#links?sel=<domain>). - The domain drawer tells one domain’s story: its rating, link count, first-seen date, and spam status, then the pages it links to — from the snapshot’s attribution edges. A mesh page jumps straight into its Pages drawer; a target that maps to no mesh page shows as its raw URL, labeled legacy URL — not a mesh page. Those legacy URLs are redirect-reclaim candidates: they still hold live links, so pointing a redirect at a live page flows that authority into the mesh — the drawer says so whenever any exist.
- The spam filter (All | Clean | Flagged as spam, with counts) splits the table by Ahrefs’ own known-spam-domain classification — the flag is theirs, carried verbatim. Flagged domains also wear a muted “flagged as spam” chip on their row (their links carry little or no weight — a young site attracting a few is normal, not an emergency).
- The “Linking to” filter scopes the table to the domains that link into one topic’s cluster pages or one page (picked by slug) — “who links to this topic?” in one control, with a clear affordance while it’s active.
- The Pages drawer’s External links block lists the page’s actual referring domains from the same edges — the verbatim referring host, subdomain included (blog.example.com stays distinct from example.com) — each with its link count and a spam chip when flagged, under the counts line.
- Apex share — on the Topics drawer — answers the question the whole model exists for: is external authority pooling at the topic’s home page, or scattered across the cluster? Paired with the health verdicts it makes “content problem vs link problem” measurable: a topic that’s worth a push, with strong content and no referring domains, needs links — the cluster’s internal anchors are already the engine’s job.
- Zero referring domains is the honest starting state for a young site. The empty states say so
rather than showing a wall of zeros — authority accrues as the mesh earns links; keep building.
A snapshot from before the
backlinkspull existed still renders every count — the drawer and the Pages list simply say the nextgtmesh pull linksadds the page-level attribution.
The channel has its own provider notion (link indexes, not search engines), so the performance
source filter doesn’t touch it: the page reads the
primary link index (Ahrefs when present) and names its source. A counts-only index (Bing) still
shows backlink counts everywhere, with the rating-and-domains sections naming what serves them.
Like every observation channel it is a read-time join — plan/apply never look at it.
Before the first snapshot
With no performance snapshot yet, Dashboard and Topics open on an empty state that walks you through the
setup: run gtmesh pull performance --source gsc, and on the first run follow the service-account walkthrough its
error message carries (the command checks its own prerequisites — see
the reference for the four steps). Once
the snapshot lands in data/raw/performance/, the running viewer picks it up and live-reloads. Until then,
Topics still lists every topic from the graph — all Quiet — Pages lists every page (statuses and the tree
come from the graph; the indexing badges and query data wait for the snapshot), and Data/Graph work as
normal. A snapshot pulled before the index sweep existed renders the same way on Pages: the list works,
and the indexing column points you at a fresh gtmesh pull performance --source gsc.
The deeper surfaces
- Data — your nodes as browsable records (Neo4j-browser-meets-Airtable). Filter by label, search by
id or property, and sort by any column — the table doubles as a ranked build queue (see
sorting). Open any node for a full detail panel: every label, every property,
and its relationships split into the two layers — the topics it belongs to (
MEMBER_OF, by axis) and the domain hubs it points at (HAS_<axis>) — plus a 1-hop neighborhood mini-graph you can click through to walk the mesh. A root topic (a pull frontier) carries a gold ★ root badge; a page node offers a jump straight to its Pages-tab drawer (queries, indexing, relations, authored source). - Graph — the whole graph, force-directed, color by label and styled by edge layer. Filter by
label, edge type, and layer (see just the topic-membership mesh, or just the domain relationships);
click a node to isolate its ego-network and walk it hop by hop. Root topics (see
topic) stand out with a gold ring and a ★-prefixed, always-on label. It’s the structure-spelunking surface — last in the tab order because Dashboard, Topics, and Pages answer the everyday questions first.
Sorting the Data table
Click a column header to sort; click it again to flip the direction. Alongside id/label/slug/status,
the table offers the two ranking columns whenever the current label scope carries them:
| Column | Lives on | Meaning |
|---|---|---|
priority | page nodes | the page’s score from scoring.formula — what next builds first |
opportunity | topic nodes | the topic’s value at stake (scoring.topic_opportunity) — see topic status |
They appear only where they mean something: select topic in the label rail and you get opportunity;
select page and you get priority. A ranking column sorts highest first on the first click.
A row with no value shows — and always sorts last, whichever way the column points — an
off-calendar page (one matching a scoring.exempt rule) has priority: null, which means unranked, never
ranked zero.
Retired pages (tombstones)
gtmesh page retire tombstones a page whose demand has faded or folded away. The node
stays in the committed graph — redirects still resolve — so the viewer keeps showing it, just clearly
marked as out of the build:
- In the Data table and detail panel a tombstone is dimmed, its id and slug struck through, and it carries a dashed † retired badge. Hovering or selecting it restores full contrast so you can still inspect it.
- In the Graph a tombstone draws as a hollow, dashed node (rather than a filled dot) with a †-prefixed, struck label, and a Tombstones legend entry counts them.
- In the Pages tab’s Tree view they are absent — a retired page isn’t served, so the compiled site tree correctly leaves it out.
The Data list is the build queue, so it hides tombstones by default. To bring them in, select
retired in the Status filter — or click the † N retired hidden — show button in the toolbar, which
selects it for you (widening an empty “all” filter to every status, so nothing else disappears). Because the
gate reads straight off the status filter, it rides the permalink: #db?status=planned,retired reopens the
table with tombstones showing.
Arguments & flags
| Option | Description | Default |
|---|---|---|
--view <view> | Initial tab: dashboard | topics | pages | links | db (Data) | graph | manifest (Pages with the Tree toggle active) | dashboard |
--port <n> | Pin the dev-server port | a free port is chosen and printed |
--open / --no-open | Force opening / not opening the browser | open on a TTY |
--env <env> | Initial render-set environment for the Pages Tree (local | dev | prod) | local |
--project <dir> | Project directory (global flag) | . |
Invariant-safety
The loopback server is a read-only dev tool, not the deterministic pipeline. It never writes committed
state (writers remain apply/seal/lifecycle), it makes no external network calls (it binds
127.0.0.1 — the performance data it shows is the committed snapshot on disk, pulled earlier by
pull), and the determinism that matters lives in the model it renders — the same artifacts always
produce the same view. Deep engineering detail is in docs/spec/gtmesh-ui-spec.md.
When there’s no graph yet
If the mesh has no committed graph (graph/nodes.jsonl), the viewer opens on an empty state pointing you at
gtmesh apply. Build the graph first, then gtmesh ui.