The GoToMesh mental models
GoToMesh has a handful of ideas you have to hold in your head to drive it well. None of them is hard, but a few are subtle enough that people build the wrong picture and then fight the tool. This page lays out the six that matter, each as what it is / why it matters / how to drive it.
If a term is unfamiliar, the glossary defines it. For how the files themselves fit together — which one owns which fact, and how they flow into the registry — see How the pieces fit. The step-by-step “which file, in what order” lives in Lifecycle and Tuning — this page is the model, not the procedure.
The mesh’s link model — which page plays which role (pillar / supporting / landing / hub / glossary) and exactly where each internal link points — is codified in the Internal Linking Rulebook . It is the conceptual source of truth behind everything on this page; its gtmesh crosswalk maps every rule onto the engine’s role/pillar/spoke terms and the doctor checks that enforce them.
1. Site type (kind)
What it is
Every mesh declares one kind in gtmesh.config.yaml. It is one of three:
kind | What it describes |
|---|---|
non_commercial | Content hubs discovered from search demand: hubs plus spokes (the bundled demo). |
commercial_catalog | Many product / category pages plus an order action; domain axes group them. |
commercial_pages | A few core service / product landings that content funnels up into. |
Why it matters
kind fixes the authority topology — the shape of how pages point at each other and which pages are the destinations. Which pages are conversion pages, which are feeders, and which are leaf spokes all follow from it. It is the very first thing the operator establishes — the first interview question — because the rest of the structure is built on top of the answer.
How to drive it
Set it once, early. With the bundled scaffold it is kind: non_commercial. When you run the operator skill after gtmesh init, it picks the kind for you (from the interview) and writes it into the config via gtmesh config. You generally do not flip kind later — it is a structural decision, not a knob. See the walkthrough for how the operator establishes it.
2. The authority layer
What it is
One field describes where a page sits in the authority topology:
role—pillar,hub,sub-hub, orspoke(informational vertex / navigational centre / intermediate / leaf — see Choosing a role).
A hub or a pillar is a topic’s apex — where authority pools. Everything else is a member of the topic and routes up to that apex. That is the whole ladder: two relationships, not a band per page. The role itself is derived from the apex edge (HUB_OF ⇒ hub, PILLAR_OF ⇒ pillar, otherwise spoke), so it can never go stale against the graph.
How a page gets its role — the engine never guesses. It reads the role from one of three places:
- A seed page (
gtmesh page add … --role hub) — a source-less page is appointed the apex of its--parent-topic, written as asource: seednode into the graph. This is where your conversion hubs normally come from. - Entity-aware classification (opt-in
classification.entity_aware) — a keyword that is only a known entity routes to that entity’s hub, so a brand/entity landing becomes an apex with no signals rule. reference/signals.csv— a keyword-pattern → role table appoints matching keywords (e.g." vs "→ asub-hubfeeder).
Anything a signals rule doesn’t match falls to the default role: spoke. So a mesh with an empty signals.csv comes out all hubs and spokes: hand-seeded and entity hubs are apexes, and everything else is a spoke — no feeder layer in between. That’s a barbell, not a funnel. To build the middle, add signals rules that appoint your feeder query shapes — comparisons, best-of, buying guides — role: sub-hub (see Tuning §4).
Then there are two different ways a page can enter the mesh, and conflating them is the single most common mistake:
seeds/<class>.csv | gtmesh page add (a seed page) | |
|---|---|---|
| What it is | A candidate TERM — a real keyword that has search demand behind it | A source-less APEX page — a hub / landing with no keyword data behind it, authored as a source: seed node |
| Where it comes from | The harvest skill, ranked against a demand corpus | You (or the operator), decreed by hand via gtmesh page add |
| Carries metrics? | Metrics are joined later from data/raw/ (the file itself never holds Vol/KD) | None — there is no keyword to measure |
| On the calendar? | Yes — it is scored and scheduled like any keyword | No — off-calendar; it exists because the topology needs it, not because demand ranked it |
This is the bit people get wrong. seeds/<class>.csv is a list of terms with demand — things people search for, that you want to rank for, scored and queued by opportunity. A seed page (gtmesh page add) decrees a page into existence — the converging hubs and commercial landings the whole mesh points at, which have no search query behind them and so are never volume-ranked. One is “what should we write because people are searching for it”; the other is “what must exist because it is the destination.”
Why it matters
The authority layer is what stops the mesh from being a flat pile of pages. Roles and topic membership decide which pages link up to which, and the apex pages you decree with gtmesh page add are the things everything else funnels toward. If you put a destination page into seeds/<class>.csv it would get scored on (non-existent) keyword volume and probably never get built; if you decree a demand keyword as a seed page you would lose its keyword data and its scoring entirely.
How to drive it
- Tune keyword → role classification in
reference/signals.csv. - Add apex / pillar pages (the destinations) with
gtmesh page add. - Fill demand-driven term classes with the
harvestskill, which writesseeds/<class>.csvfor your review.
Deeper procedure is in Tuning.
3. Slug anchoring, freeze, and rename
What it is
A page’s slug names its HEAD keyword — the volume-ranked primary keyword for that page (set by identity: { anchor: head } in the config), not a divergent source parent_topic. The important property: once a slug is committed, it is frozen. It becomes sticky committed state.
So what changes a URL, and what doesn’t:
| Event | Does the URL move? |
|---|---|
| The head keyword flips on a re-pull (a higher-volume keyword takes the lead) | No. The committed slug stays put — no URL churn. |
The source parent_topic changes | Triggers a redirect action (human-gated). |
A rename group-edit in overrides/group-edits.yaml | Yes — this is the deliberate lever. |
Why it matters
URLs are expensive to change — they break links and rankings. If the slug followed the head keyword on every re-pull, your URLs would churn every time the keyword tool reshuffled volumes. Freezing the slug means the catalogue can keep refreshing keyword data underneath a page without ever moving its address by accident.
How to drive it
The only lever that intentionally moves a committed URL is a rename group-edit in overrides/group-edits.yaml. A rename:
- pins the new slug,
- keeps the page’s identity (it is the same page),
- emits a 301 redirect — the old URL becomes a
urlnode with anALIAS_OFedge to the live page, so old URLs resolve through it forever, - and moves the page bundle (the written body and its co-located assets) to the new path.
So: re-pulls never move a URL; only an explicit rename does. The full slug-change / removal detail is in Lifecycle.
4. The SERP layer model
What it is
Some pages carry a funnel value describing the SERP layer they belong to — what the search results for that query actually look like:
commercial— the SERP is dominated by landing / product pages (this query is a money page).content— the SERP is informational; this page funnels up into a commercial landing (viafunnel_target, a slug orparent_topic).mixed— the SERP is split roughly 50/50; a human call, never auto-resolved.
funnel_target names the landing a content page links up to. Both are frozen at discovery time — the operator classifies them once (by tearing down the real SERP with gtmesh pull serp) and the engine simply applies them. It does not re-derive the layer on every plan.
Why it matters
This is how money pages avoid being buried behind blog posts. A commercial-layer landing ships off-calendar, by business priority — it is exempt from volume scoring (see scoring.exempt with when: { funnel: commercial } in the config). Without this, a high-value landing page with modest search volume would queue behind a pile of higher-volume informational articles and never get built. The SERP layer says: this is a destination, ship it because the business needs it, not because a volume number ranked it.
How to drive it
You normally don’t set funnel by hand — the operator establishes it from the SERP teardown and writes it into seeds/<class>.csv / the seed page it authors (gtmesh page add). The lever you do hold is scoring.exempt in gtmesh.config.yaml, which is what makes the whole commercial layer ship off-calendar. (On a non_commercial mesh nothing carries funnel: commercial, so the rule is inert.) See the walkthrough.
5. Fan-out vs fragmentation
What it is
One parent_topic can legitimately fan out into several typed pages — a glossary entry and a guide for the same term, for example. That is a feature: different page types serve different intents for the same topic.
Fragmentation is the failure mode: a single stray keyword minting a junk extra page that nobody needs.
The guard between them is classification.min_group_members (with a companion, min_group_volume_pct) in gtmesh.config.yaml. A typed sibling group must clear either bar to spawn its own page — at least min_group_members members, or at least min_group_volume_pct of the parent’s volume. If it clears neither, it folds back into the parent’s dominant page as brief material instead of becoming its own page.
# gtmesh.config.yaml → classification
# min_group_members: 2 # default 1 = off; set 2 to fold single-member fragments
# min_group_volume_pct: 10Why it matters
Programmatic content lives or dies on this line. Fan-out gives you good coverage (a term explained in a glossary and walked through in a guide); fragmentation gives you thin, near-duplicate pages that dilute the mesh and look like spam. The guard lets you keep the first and stop the second.
How to drive it
The default is min_group_members: 1, which is a no-op (every group spawns). Set it to 2 to fold single-member fragments back into their parent. Tune min_group_volume_pct alongside it if you’d rather gate on share-of-volume. Watch the plan’s output as you change it. More in Tuning.
A related axis is entity fan-out. With classification.entity_aware on (opt-in), every spelling and phrasing of a known entity is pulled under that entity’s canonical hub before the guard runs: brand variants collapse into the one hub, and a real sub-intent («brand + скачать») becomes a sub-page nested under it rather than a sibling of unrelated topics. Because the hub and its sub-pages then share a parent_topic, the same fan-out guard applies — and when the dominant sibling is the hub, a below-threshold sub-intent is dropped (not folded into the hub’s brief), so a stray low-demand intent can’t pollute the hub. See Tuning › Entity-aware classification.
One URL, one page — keyword variants
A keyword tool will happily hand you the same query twice, wearing different punctuation: self priming centrifugal pump and self-priming centrifugal pump, or submersible pump - screwfix and submersible pump screwfix. Slugs strip punctuation, so both forms want the same URL — and only one page can live there.
When the two head keywords are the same query differently punctuated, plan folds them: volumes sum, the highest-volume form becomes the primary_keyword, and the other rides along as a secondary keyword. Nothing is discarded, and a variant-fold warning names what merged.
warn: /glossary/self-priming-centrifugal-pump
← "self priming centrifugal pump" (150) + "self-priming centrifugal pump" (60)When it can’t prove they’re one query — the heads are genuinely different, or one slug is already pinned by a rename, or the pages came from a split — it says so rather than guessing, and names your two levers in overrides/group-edits.yaml:
merges— they are one page. Fold them into a single identity; volumes sum.renames— they’re distinct pages that collided by accident. Pin one to its own URL.
Both apply before identity is resolved, so either one clears the collision on the next plan.
6. Winning a topic — the head is the prize, the tail is the ladder
What it is
A topic is not won by ranking one page for one keyword. It’s won by a cluster: members route authority up to the apex, and the apex becomes able to hold the head term. So “can I win this topic?” is a question about the topic’s whole demand shape, not the difficulty of its head keyword.
Two numbers, answering two different questions:
- The head carries the value. A topic’s
opportunitycomes overwhelmingly from its head term — that’s where the volume and the CPC are. The head is the prize. - The tail tells you whether you can start. The uncovered keywords that restate the topic’s head/seed terms — mostly questions, mostly little or no CPC of their own — are the ladder. Enough of them at low difficulty means there’s a way in: you cover them, authority pools at the apex, and the head comes into reach later.
So a hard head is a horizon, not a veto. A KD-80 head with 3,000 volume of uncovered KD-10 questions beneath it is a good investment — you win the tail first and climb. The same head with nothing beneath it is the one genuine skip: no entry point, no ladder.
Two ways to get this wrong, and they fail in opposite directions. Judging a topic by its head’s difficulty
alone makes you walk away from money you could reach in stages. Judging the tail by its own commercial
value makes you walk away from the ladder — those question terms score near zero on volume × cpc because
nobody bids on them, which is exactly what you’d expect from a rung rather than a destination.
Why it matters
It’s what makes the build order right, and the build order is most of the game: apex first (authority
needs somewhere to pool), then the winnable tail, and only then contend for the head. It’s also why
gtmesh next is apex-first and paced — see The refresh loop.
How to drive it
gtmesh topic candidatesis the selection gate — it ranks topics and gives each a verdict (build / promote / invest / hold).gtmesh topic fill <value>names a topic’s uncovered demand — the rungs you haven’t built yet, content and commercial.gtmesh nextturns the choice into a paced build batch.
How the engine measures it
gtmesh topic candidates computes both sides and takes the better of them:
| Question | How it’s measured | |
|---|---|---|
| head | Can we take the head term directly? | The head keyword’s own difficulty, read from the demand bag |
| tail | Is there a way in? | The median difficulty of the topic’s uncovered demand |
winnability = max(head, tail) — which is what makes a hard head a horizon rather than a veto. The output
names which side carried it (via head / via tail) and, when there’s a ladder, prints the easiest rungs:
way in: "how to prime a submersible pump" (KD 5, 300 vol) · ….
Three deliberate choices behind that:
- Median, not minimum. A single stray KD-0 term shouldn’t declare a whole topic winnable. The median asks “what does a typical rung cost?”
- Uncovered only. A keyword some page already targets isn’t a rung left to climb. (Whether that page is
built yet is
gtmesh next’s question, not this one.) - Unscored ≠ easy. A keyword the provider never rated carries no difficulty at all, so it counts as uncovered demand but never as evidence of an easy way in.
When you’ll see head KD — or tail KD —. Difficulty is genuinely optional data: providers return no
score for much of the low-volume long tail, and gtmesh records that as unknown rather than 0. A topic with
no difficulty evidence on either side scores win 1.00 and reports (no KD data) — the ranking is then
effectively opportunity-only, which is the honest answer when nothing is known. A tail KD — with
0 vol uncovered means something different and good: every keyword under that topic already has a page.
7. The invariants
What it is
A short list of properties GoToMesh guarantees and will not break:
- The CLI is deterministic and LLM-free. Same inputs → byte-identical graph. The only step that uses a model is the
article-writerskill, which writes page bodies. - The committed graph IS the state file.
graph/nodes.jsonl+graph/edges.tsvare master state — never hand-edited. You author your domain into it through validated verbs (gtmesh graph upsertfor entities,gtmesh page addfor source-less pages);apply,seal, and the lifecycle commands are the derived mesh’s only writers. planis read-only. It computes the diff and prints it; it writes only a derived, git-ignored planfile. It never changes state.- Discovery is
import, notplan. The open-ended, networked research (theoperatorandharvestskills) runs outside the deterministic loop and emits committed, human-reviewed files. The engine then consumes those files deterministically.
Why it matters
These are the reasons you can trust the mesh: re-running apply is always safe, the diff is reviewable because state is a committed file, and the one non-deterministic, model-driven step (writing prose) is firewalled off from the structural engine. The boundary between “the deterministic engine” and “the model-driven writer / researcher” is the whole design.
How to drive it
You don’t drive the invariants — you rely on them. Concretely: never hand-edit the committed graph (graph/nodes.jsonl, graph/edges.tsv); use plan freely (it costs nothing and changes nothing); and treat the files the operator / harvest write as input you review and commit, not as engine state. The lifecycle that flows from these rules is in Lifecycle.