Tuning
GoToMesh never guesses how to classify a keyword. If no rule covers a keyword, the engine surfaces it as unresolved rather than inventing a section for it. So tuning a mesh isn’t a one-time setup — it’s a tight loop driven by what the plan tells you is missing.
This page is the practical guide: the plan says X is unresolved — which file do I edit? It consolidates the levers in the order you’d reach for them. (For the deeper why behind the shape of the config, see mental models.)
Every generated mesh also ships its own TUNING.md at the repo root — the same guidance,
living next to the files it points at. This page is the hosted copy.
The tuning rhythm
gtmesh extract --cluster <id> # pull keyword data (once; immutable, re-plan freely)
gtmesh plan # prints the plan to stdout (pipe to a file with `> plan.md` if you like)In the printed plan, read two sections:
- “Catalogue / create — by page type” — is the shape right? Too many of one type?
- “Unresolved — N page(s) need a config rule” — each entry names a keyword with no section/page_type.
Then adjust the files below and re-plan. plan is read-only and free — iterate as much as you
like; nothing is written until you apply. Unresolved is your to-do list: each entry is either a
keyword pattern you haven’t mapped, or junk you should exclude.
The knobs, in the order to tune them
1. gtmesh.config.yaml › taxonomy
The structural skeleton. Touch this first because everything else routes against it.
sections— your top-level URL areas (e.g.guides,compare,glossary).clusters— grouping axes (category,topic). Used for siblings and cluster-based mesh links.intents— order matters. Classify picks the first intent present on a keyword, so put the most discriminating intents first.informationalis near-universal — put it last, or it swamps the topic cluster. To override the global order for one section, setclassification.section_intent(e.g.glossary: informational, so a “what is X” page isn’t taggedbrandedjust because it names a brand).tiers+authority— the funnel bands (A conversion → B feeder → D spoke) and how authority flows.identity.anchor— page URL reads off the wrong keyword?head(default) names the slug after the page’s head keyword;parent_topicuses the source cluster label. A slug is frozen once committed (a head-flip on re-extract won’t churn the URL) — to deliberately move a URL, use arenamegroup-edit (it pins the new slug, keeps the page, and emits a 301).- Junk extra page from one stray keyword? Set
classification.min_group_members: 2(optionallymin_group_volume_pct) to fold a below-threshold typed sibling back into the parent’s dominant page. Real fan-out with demand is kept.
2. reference/entities.csv
Your domain’s entities (brands, products, categories). This is load-bearing for the mesh:
entity resolution sets a row’s category cluster, and a spoke links up to the hub for the same
entity (e.g. a glossary page about X → the X hub). Make it comprehensive, or up-links won’t form.
When a keyword names more than one entity, the winner is chosen by
classification.entity_class_priority (highest class first), then longest match — never by row
order. So set that priority for your domain and order the CSV however you like.
An entity contributes a value to every taxonomy.clusters axis it has a column for — so a
catalogue can give entities.csv a brand column (and declare a brand cluster axis), letting a
product page roll up by brand as well as category.
3. gtmesh.config.yaml › sections_map
Ordered when → then rules: a keyword pattern → a section. First match wins. A match value
is a case-insensitive substring, or an anchored /regex/ for precision. Anything matching no
rule → unresolved. This is the file you edit most when working down the unresolved list — add
rules until what’s left is only genuine junk.
4. reference/signals.csv
Keyword pattern → role (hub / sub-hub / spoke) + tier (A / B / D). Reserve
hub/A for true conversion pages — let the long tail be feeders and spokes. (Real Tier-A
hubs usually come from seed-pages, below, not from keyword signals.) Role and tier are derived
before page_type, so a page_type can require when: { role: hub }.
5. gtmesh.config.yaml › page_types
Each entry: a when (match by section and/or role/tier) → a template + schema. Ordered;
first match wins. For any new type, author its templates/<type>.md (prose theory) +
schemas/<type>.schema.yaml (structure). Use schema_variants + variant_when for
variant-by-peer.
The per-type schemas (schemas/<type>.schema.yaml) are yours — extend or add freely;
gtmesh upgrade never touches them. They share a base, schemas/common.schema.yaml, which is
engine-owned and refreshed on upgrade (if you’ve edited it, the new version lands as
common.schema.yaml.new to merge).
6. reference/scope.yaml + adapters.ahrefs.exclude_substrings
Filter out what you’ll never build pages for — navigational / brand-asset / off-topic queries
(login, logo, retailer names, sizing charts…). They work at different stages:
exclude_substringsdrops them at extract — saves API credits.scope.yamlparks them tobacklogat plan — recoverable.
Either way, scoped-out rows drop off the unresolved list.
7. reference/seed-pages.csv
Source-less pages — apex/pillar/conversion hubs with no keyword behind them. In v1 your Tier-A
hubs live here (give each an intent so it clusters). These are the destinations the keyword long
tail links up to.
8. gtmesh.config.yaml › scoring + transforms
scoring.formularanks rows intopriority;scoring.exemptmarks off-calendar rows (priority = null);commercial_bonusweights by CPC. (A SERP-classifiedcommercial-layer landing is exempt — it ships by business priority, not volume, so money pages aren’t buried behind volume-ranked blog content. Inert for a non-commercial mesh.)transforms/*.ts— pure, row-local derivations for deterministic project columns (e.g. a computed score band). None ship by default. Titles are not here —metaTitle/title/navTitle/metaDescriptionare writer-authored editorial meta (config.authored_meta); the LLM writes good titles, with rules infoundation/editorial.md. Onlyslugis deterministic.
9. gtmesh.config.yaml › aeo (the citability gate)
The engine-owned review-gate skill scores every draft for AI-answer-engine citability. You tune
the numbers here — dimensions[] (weights + per-dimension threshold), overall_threshold,
and the answer_block word range — never by editing the skill (upgrade keeps it current).
The voice the gate pulls toward is foundation/voice.md; the banned words are
reference/editorial-rules.yaml. The gate and the humanizer skill both read those, so retune
voice there, not in the skills.
10. Images — foundation/art-direction.md + gtmesh.config.yaml › images
If a page type has image slots (e.g. hero, gallery), the article-writer authors art-direction
briefs and the engine-owned image-director skill generates + places the files (an explicit
step).
- Tune the look in
foundation/art-direction.md(registers, prompts, negative prompt, per-type briefs). - Tune the knobs in
gtmesh.config.yamlimages:(gen model,registers.<name>.ratio, per-registersettings).
The register names in the two files must match. You never edit the skill.
Rubrics — gtmesh.config.yaml › rubrics
For rubric-bearing page types (review / comparison / best-for), rubrics declares the scored
dimensions the writer evaluates into the page’s rubric. Retune the axes here, never by
editing the template. Keep them concrete and few (3–6) so pages stay comparable; a page type with
no entry scores no rubric.
Then run the loop
Once plan looks right:
gtmesh apply # catalogue everything (previews + asks to confirm)
gtmesh promote --section <x> # choose a batch to build (sets status; --dry-run previews; demote reverses)
gtmesh apply # scaffold the promoted rows → writing
gtmesh status # the worklist (pages awaiting a body)
# → the article-writer skill fills content/<slug>/index.yaml, runs `gtmesh validate`, humanizer, review-gate
gtmesh seal <slug> # writing → review (schema-valid + lint-clean only)
gtmesh publish <slug> # → publishedFor what each of those steps and statuses means in full, see lifecycle & reconcile.
Guardrails
- Never hand-edit
registry/pages.csv—apply/seal/promote/ lifecycle are its only writers. - The pipeline is deterministic and LLM-free except the article-writer step.
- Structural mesh links render as nav, not prose — they live in the registry, not the page
YAML. Don’t write up/sibling links into the body. A relink is a deterministic
restampthat touches only the hash — the page file is untouched. - Changing a content-determining field →
rewrite(the writer reruns the body); changing the render projection (slug, mesh links, an in-projection column) →restamp(deterministic, no writer). That’s why the registry carries two hashes.