From nodeshub-seo-skills
Clusters keywords by SERP similarity using Google results (NodesHub) or semantic embeddings (OpenRouter). Includes Louvain-based grouping and LLM-generated cluster names.
How this skill is triggered — by the user, by Claude, or both
Slash command
/nodeshub-seo-skills:nod-serp-clustersThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Two clustering methods available. **Always ask the user which method they want before running.**
Two clustering methods available. Always ask the user which method they want before running.
cluster.pyKeywords sharing the same Google top-10 results belong to the same cluster. Uses Weighted Jaccard + Louvain.
cluster_semantic.pyKeywords with similar meaning belong to the same cluster. Uses OpenRouter embeddings + cosine similarity + Louvain.
| Scenario | Recommended method |
|---|---|
| "Which keywords can target the same page?" | SERP-based — Google decides |
| "Group keywords by topic/meaning" | Semantic — faster and cheaper |
| "Find cannibalization" | SERP-based — needs actual SERP data |
| "Quick clustering, no SERP budget" | Semantic |
| "Most accurate content mapping" | Both — compare results |
# === SERP-based clustering ===
python3 .claude/skills/nod-serp-clusters/scripts/cluster.py keywords.csv --gl pl --hl pl
python3 .claude/skills/nod-serp-clusters/scripts/cluster.py keywords.csv --gl pl --hl pl --levels 3 --report html
python3 .claude/skills/nod-serp-clusters/scripts/cluster.py keywords.csv --gl pl --hl pl --workers 3 --budget 200
# Rerun without re-fetching SERPs (uses cache from previous run)
python3 .claude/skills/nod-serp-clusters/scripts/cluster.py keywords.csv --gl pl --hl pl --levels 3 --report html
# Force fresh SERP fetch (ignore cache)
python3 .claude/skills/nod-serp-clusters/scripts/cluster.py keywords.csv --gl pl --hl pl --no-cache
# === Semantic clustering (no NodesHub tokens needed) ===
python3 .claude/skills/nod-serp-clusters/scripts/cluster_semantic.py keywords.csv --threshold 0.25 --levels 3
python3 .claude/skills/nod-serp-clusters/scripts/cluster_semantic.py keywords.csv --output clusters_semantic.csv --json
For each keyword pair, compare top-10 SERP results:
1 / log2(pos + 2) — pos 1 has weight 1.44, pos 10 has 0.43Instead of hardcoded blacklists, domains are weighted by how often they appear:
domain_weight = 1 / sqrt(coverage), min 0.2Graph with keywords as nodes, weighted Jaccard as edge weights. Louvain finds natural communities without chain clustering (a problem with agglomerative methods).
OpenRouter generates 2-5 word cluster names based on keyword samples.
Requires two API keys:
/connect-nodeshub./connect-openrouter.# Verify both keys
python3 .claude/skills/nod-nodeshub-api/scripts/check_setup.py
Always present these parameters to the user before running, so they understand what each one does.
| Parameter | What it does | Default | Effect |
|---|---|---|---|
--threshold T | Min weighted Jaccard similarity to connect two keywords | 0.55 | Lower = more keywords grouped together, higher = tighter clusters. 0.55 ≈ 7/10 shared URLs |
--levels N | Clustering depth (1-3) | 1 | 1 = single flat clustering, 2 = broad + specific, 3 = broad + medium + specific hierarchy |
--domain-bonus F | Bonus for same-domain-different-URL match | 0.3 | Higher = more weight to domain-level similarity (not just exact URL). 0 = URL-only matching |
--resolution F | Override Louvain resolution for all levels | auto | Higher = more clusters, lower = fewer bigger clusters. Overrides per-level defaults |
| Parameter | What it does | Default | Effect |
|---|---|---|---|
--top-n N | Only cluster top N keywords (sorted by serp_overlap) | 0 (all) | Useful to focus on most important keywords and save tokens |
--budget N | Max NodesHub tokens to spend | no limit | Hard stop for SERP fetching |
--workers N | Concurrent SERP requests | 4 | 1 = sequential (safe), 4 = default, 8 = aggressive. Higher = faster |
--gl | Country code for SERP | pl | Match to target market |
--hl | Language code | pl | Match to target language |
| Parameter | What it does | Default | Effect |
|---|---|---|---|
--report html | Generate HTML report with dendrogram, domain visibility, SERP features | off | Interactive D3.js dendrogram with collapsible levels |
--report md | Generate Markdown report with text tree | off | Same data, text format |
--json | Also output JSON file | off | Machine-readable cluster data |
--output PATH | Custom output CSV path | auto | Default: {input_stem}_clustered.csv |
--model | OpenRouter model for cluster naming | google/gemini-2.5-flash-lite | Any OpenRouter model ID |
--report)HTML reports can display your company logo and use your brand colors. To customize:
assets/branding/brand-config.json — set company name, colors, fontsassets/branding/logo-light.svg and logo-dark.svg with your logosThe report header shows your logo + report title + date; the footer shows your company name + generation timestamp. If no branding is configured, default Nodeshub styling is used.
To extract brand styles from your website automatically, run assets/branding/extract-brand-styles.js in browser DevTools.
With --levels 2 or --levels 3, the script clusters at multiple thresholds simultaneously. Each keyword gets assigned to a cluster at each level. SERP data is fetched only once — levels only affect the clustering step.
| Level | --levels 2 | --levels 3 | Resolution | Purpose |
|---|---|---|---|---|
| L1 (broad) | threshold×0.5, res=0.5 | threshold×0.4, res=0.3 | Low = fewer big clusters | Pillar topics, content silos |
| L2 (medium) | — | threshold×0.7, res=0.8 | Medium | Subtopic groups |
| L3 (specific) | threshold×1.0, res=1.0 | threshold×1.0, res=1.0 | Standard | Individual pages |
CSV output columns per level: {level}_id, {level}_name, {level}_size
Example with --levels 3:
keyword, L1_broad_id, L1_broad_name, L2_medium_id, L2_medium_name, L3_specific_id, L3_specific_name
"seo cennik", 0, "SEO uslugi", 3, "Cennik pozycjonowania", 12, "Koszty SEO"
| Column | Description |
|---|---|
cluster_id | Numeric cluster ID (0 = largest cluster) |
cluster_name | LLM-generated descriptive name for the cluster |
cluster_size | Number of keywords in this cluster |
| Threshold | ~Shared URLs | Clustering behavior | Use when |
|---|---|---|---|
| 0.22 | ~2/10 | Very loose — large topic buckets | Pillar pages, content silos |
| 0.35 | ~4/10 | Loose — broad subtopic groups | Content planning |
| 0.45 | ~5/10 | Medium — related keywords | Subtopic mapping |
| 0.55 | ~7/10 | Default — strong SERP overlap | Standard SEO clustering |
| 0.65 | ~8/10 | Strict — near-identical SERPs | 1-page-per-cluster mapping |
| 0.75+ | ~9/10 | Very strict — true synonyms only | Cannibalization analysis |
/nod-keyword-research to get a keyword CSVAfter collecting data, ask the user:
"Add results to an HTML report?"
- New report — creates a branded HTML report in
reports/- Existing report — appends a section to a chosen report
- Skip — no report
Use render_report_section(all_level_results, all_serps, all_snippets) from cluster.py, then create_report() or append_section() from report.py. Note: render_report_section returns (section_html, extra_head) — pass extra_head to create_report(extra_head=extra_head).
npx claudepluginhub senuto/nodeshub-seo-skills --plugin nodeshub-seo-skillsClusters raw keyword lists into SEO topic groups (pillar + cluster pages) by intent, using Ahrefs data or heuristic grouping.
Groups keywords by Google SERP overlap (not text similarity) to design hub-and-spoke content clusters with internal link matrices and interactive visualizations. Useful for content architecture planning.
Clusters keywords by Google SERP overlap for hub-and-spoke content architecture. Generates internal link matrices and interactive visualizations. Useful for topic clusters, pillar pages, keyword grouping.