antigravity-seo-kit 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of antigravity-seo-kit might be problematic. Click here for more details.
- package/.agent/agent.md +96 -0
- package/.agent/skills/seo/SKILL.md +153 -0
- package/.agent/skills/seo/references/cwv-thresholds.md +108 -0
- package/.agent/skills/seo/references/eeat-framework.md +214 -0
- package/.agent/skills/seo/references/local-schema-types.md +230 -0
- package/.agent/skills/seo/references/local-seo-signals.md +218 -0
- package/.agent/skills/seo/references/maps-api-endpoints.md +160 -0
- package/.agent/skills/seo/references/maps-free-apis.md +176 -0
- package/.agent/skills/seo/references/maps-gbp-checklist.md +150 -0
- package/.agent/skills/seo/references/maps-geo-grid.md +154 -0
- package/.agent/skills/seo/references/quality-gates.md +155 -0
- package/.agent/skills/seo/references/schema-types.md +118 -0
- package/.agent/skills/seo/schema/templates.json +213 -0
- package/.agent/skills/seo/scripts/analyze_visual.py +217 -0
- package/.agent/skills/seo/scripts/capture_screenshot.py +181 -0
- package/.agent/skills/seo/scripts/fetch_page.py +196 -0
- package/.agent/skills/seo/scripts/parse_html.py +201 -0
- package/.agent/skills/seo-audit/SKILL.md +278 -0
- package/.agent/skills/seo-competitor-pages/SKILL.md +212 -0
- package/.agent/skills/seo-content/SKILL.md +230 -0
- package/.agent/skills/seo-dataforseo/SKILL.md +418 -0
- package/.agent/skills/seo-geo/SKILL.md +305 -0
- package/.agent/skills/seo-google/SKILL.md +405 -0
- package/.agent/skills/seo-google/assets/templates/cwv-audit-report.md +48 -0
- package/.agent/skills/seo-google/assets/templates/gsc-performance-report.md +44 -0
- package/.agent/skills/seo-google/assets/templates/indexation-status-report.md +43 -0
- package/.agent/skills/seo-google/references/auth-setup.md +154 -0
- package/.agent/skills/seo-google/references/ga4-data-api.md +184 -0
- package/.agent/skills/seo-google/references/indexing-api.md +107 -0
- package/.agent/skills/seo-google/references/keyword-planner-api.md +66 -0
- package/.agent/skills/seo-google/references/nlp-api.md +55 -0
- package/.agent/skills/seo-google/references/pagespeed-crux-api.md +204 -0
- package/.agent/skills/seo-google/references/rate-limits-quotas.md +75 -0
- package/.agent/skills/seo-google/references/search-console-api.md +156 -0
- package/.agent/skills/seo-google/references/supplementary-apis.md +99 -0
- package/.agent/skills/seo-google/references/youtube-api.md +49 -0
- package/.agent/skills/seo-google/scripts/crux_history.py +321 -0
- package/.agent/skills/seo-google/scripts/ga4_report.py +478 -0
- package/.agent/skills/seo-google/scripts/google_auth.py +795 -0
- package/.agent/skills/seo-google/scripts/google_report.py +2273 -0
- package/.agent/skills/seo-google/scripts/gsc_inspect.py +340 -0
- package/.agent/skills/seo-google/scripts/gsc_query.py +378 -0
- package/.agent/skills/seo-google/scripts/indexing_notify.py +313 -0
- package/.agent/skills/seo-google/scripts/keyword_planner.py +297 -0
- package/.agent/skills/seo-google/scripts/nlp_analyze.py +309 -0
- package/.agent/skills/seo-google/scripts/pagespeed_check.py +649 -0
- package/.agent/skills/seo-google/scripts/youtube_search.py +355 -0
- package/.agent/skills/seo-hreflang/SKILL.md +192 -0
- package/.agent/skills/seo-image-gen/SKILL.md +211 -0
- package/.agent/skills/seo-image-gen/references/cost-tracking.md +47 -0
- package/.agent/skills/seo-image-gen/references/gemini-models.md +200 -0
- package/.agent/skills/seo-image-gen/references/mcp-tools.md +115 -0
- package/.agent/skills/seo-image-gen/references/post-processing.md +192 -0
- package/.agent/skills/seo-image-gen/references/presets.md +69 -0
- package/.agent/skills/seo-image-gen/references/prompt-engineering.md +411 -0
- package/.agent/skills/seo-image-gen/references/seo-image-presets.md +137 -0
- package/.agent/skills/seo-image-gen/scripts/batch.py +97 -0
- package/.agent/skills/seo-image-gen/scripts/cost_tracker.py +191 -0
- package/.agent/skills/seo-image-gen/scripts/edit.py +141 -0
- package/.agent/skills/seo-image-gen/scripts/generate.py +149 -0
- package/.agent/skills/seo-image-gen/scripts/presets.py +153 -0
- package/.agent/skills/seo-image-gen/scripts/setup_mcp.py +151 -0
- package/.agent/skills/seo-image-gen/scripts/validate_setup.py +133 -0
- package/.agent/skills/seo-images/SKILL.md +176 -0
- package/.agent/skills/seo-local/SKILL.md +381 -0
- package/.agent/skills/seo-maps/SKILL.md +328 -0
- package/.agent/skills/seo-page/SKILL.md +86 -0
- package/.agent/skills/seo-plan/SKILL.md +118 -0
- package/.agent/skills/seo-plan/assets/agency.md +175 -0
- package/.agent/skills/seo-plan/assets/ecommerce.md +167 -0
- package/.agent/skills/seo-plan/assets/generic.md +144 -0
- package/.agent/skills/seo-plan/assets/local-service.md +160 -0
- package/.agent/skills/seo-plan/assets/publisher.md +153 -0
- package/.agent/skills/seo-plan/assets/saas.md +135 -0
- package/.agent/skills/seo-programmatic/SKILL.md +171 -0
- package/.agent/skills/seo-schema/SKILL.md +223 -0
- package/.agent/skills/seo-sitemap/SKILL.md +180 -0
- package/.agent/skills/seo-technical/SKILL.md +211 -0
- package/.agent/workflows/seo-audit.md +17 -0
- package/.agent/workflows/seo-competitor-pages.md +12 -0
- package/.agent/workflows/seo-content.md +14 -0
- package/.agent/workflows/seo-geo.md +12 -0
- package/.agent/workflows/seo-google.md +12 -0
- package/.agent/workflows/seo-hreflang.md +12 -0
- package/.agent/workflows/seo-images.md +13 -0
- package/.agent/workflows/seo-local.md +12 -0
- package/.agent/workflows/seo-maps.md +11 -0
- package/.agent/workflows/seo-page.md +13 -0
- package/.agent/workflows/seo-plan.md +13 -0
- package/.agent/workflows/seo-programmatic.md +12 -0
- package/.agent/workflows/seo-schema.md +11 -0
- package/.agent/workflows/seo-sitemap.md +9 -0
- package/.agent/workflows/seo-technical.md +18 -0
- package/LICENSE +88 -0
- package/README.md +122 -0
- package/bin/cli.js +117 -0
- package/docs/ARCHITECTURE.md +218 -0
- package/docs/COMMANDS.md +184 -0
- package/docs/INSTALLATION.md +100 -0
- package/docs/MCP-INTEGRATION.md +153 -0
- package/docs/TROUBLESHOOTING.md +151 -0
- package/docs/superpowers/plans/2026-03-13-github-audit-fixes.md +511 -0
- package/extensions/banana/README.md +95 -0
- package/extensions/banana/docs/BANANA-SETUP.md +86 -0
- package/extensions/banana/install.sh +170 -0
- package/extensions/banana/references/cost-tracking.md +47 -0
- package/extensions/banana/references/gemini-models.md +200 -0
- package/extensions/banana/references/mcp-tools.md +115 -0
- package/extensions/banana/references/post-processing.md +192 -0
- package/extensions/banana/references/presets.md +69 -0
- package/extensions/banana/references/prompt-engineering.md +411 -0
- package/extensions/banana/references/seo-image-presets.md +137 -0
- package/extensions/banana/scripts/batch.py +97 -0
- package/extensions/banana/scripts/cost_tracker.py +191 -0
- package/extensions/banana/scripts/edit.py +141 -0
- package/extensions/banana/scripts/generate.py +149 -0
- package/extensions/banana/scripts/presets.py +153 -0
- package/extensions/banana/scripts/setup_mcp.py +151 -0
- package/extensions/banana/scripts/validate_setup.py +133 -0
- package/extensions/banana/uninstall.sh +43 -0
- package/extensions/dataforseo/README.md +169 -0
- package/extensions/dataforseo/docs/DATAFORSEO-SETUP.md +74 -0
- package/extensions/dataforseo/field-config.json +280 -0
- package/extensions/dataforseo/install.ps1 +110 -0
- package/extensions/dataforseo/install.sh +161 -0
- package/extensions/dataforseo/uninstall.ps1 +35 -0
- package/extensions/dataforseo/uninstall.sh +39 -0
- package/lib/api.js +190 -0
- package/lib/fingerprint.js +68 -0
- package/lib/installer.js +486 -0
- package/lib/utils.js +254 -0
- package/package.json +40 -0
- package/pyproject.toml +11 -0
- package/requirements-google.txt +15 -0
- package/requirements.txt +11 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Google Ads API - Keyword Planner for SEO keyword research.
|
|
4
|
+
|
|
5
|
+
Gold-standard source for keyword search volume, CPC, and competition data.
|
|
6
|
+
Requires a Google Ads Manager account with a developer token.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python keyword_planner.py ideas "seo tools" --json
|
|
10
|
+
python keyword_planner.py volume "seo tools,seo audit,seo checker" --json
|
|
11
|
+
python keyword_planner.py forecast "seo tools" --json
|
|
12
|
+
|
|
13
|
+
Prerequisites:
|
|
14
|
+
- Google Ads Manager account (can be free)
|
|
15
|
+
- Developer Token (apply at Google Ads API Center)
|
|
16
|
+
- OAuth credentials or service account
|
|
17
|
+
- google-ads Python library: pip install google-ads
|
|
18
|
+
- Config: ~/.config/claude-seo/google-api.json with:
|
|
19
|
+
{
|
|
20
|
+
"ads_developer_token": "YOUR_DEV_TOKEN",
|
|
21
|
+
"ads_customer_id": "123-456-7890",
|
|
22
|
+
"ads_login_customer_id": "123-456-7890"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
Note: Accounts without active ad spend receive bucketed volume ranges
|
|
26
|
+
(e.g., "1K-10K") instead of exact numbers.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
import argparse
|
|
30
|
+
import json
|
|
31
|
+
import sys
|
|
32
|
+
from typing import Optional
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
from google.ads.googleads.client import GoogleAdsClient
|
|
36
|
+
from google.ads.googleads.errors import GoogleAdsException
|
|
37
|
+
HAS_GOOGLE_ADS = True
|
|
38
|
+
except ImportError:
|
|
39
|
+
HAS_GOOGLE_ADS = False
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
from google_auth import load_config
|
|
43
|
+
except ImportError:
|
|
44
|
+
import os
|
|
45
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
46
|
+
from google_auth import load_config
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _build_ads_client() -> Optional[object]:
|
|
50
|
+
"""Build Google Ads client from config."""
|
|
51
|
+
if not HAS_GOOGLE_ADS:
|
|
52
|
+
print(
|
|
53
|
+
"Error: google-ads library required. Install with: pip install google-ads",
|
|
54
|
+
file=sys.stderr,
|
|
55
|
+
)
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
config = load_config()
|
|
59
|
+
dev_token = config.get("ads_developer_token")
|
|
60
|
+
customer_id = config.get("ads_customer_id", "").replace("-", "")
|
|
61
|
+
login_customer_id = config.get("ads_login_customer_id", "").replace("-", "")
|
|
62
|
+
oauth_client_path = config.get("oauth_client_path")
|
|
63
|
+
|
|
64
|
+
if not dev_token:
|
|
65
|
+
print(
|
|
66
|
+
"Error: No Google Ads developer token configured. "
|
|
67
|
+
"Add 'ads_developer_token' to ~/.config/claude-seo/google-api.json. "
|
|
68
|
+
"Get a token at: https://ads.google.com/aw/apicenter",
|
|
69
|
+
file=sys.stderr,
|
|
70
|
+
)
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
if not customer_id:
|
|
74
|
+
print(
|
|
75
|
+
"Error: No Google Ads customer ID configured. "
|
|
76
|
+
"Add 'ads_customer_id' (format: 123-456-7890) to config.",
|
|
77
|
+
file=sys.stderr,
|
|
78
|
+
)
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
# Build from dict configuration
|
|
83
|
+
ads_config = {
|
|
84
|
+
"developer_token": dev_token,
|
|
85
|
+
"use_proto_plus": True,
|
|
86
|
+
}
|
|
87
|
+
if login_customer_id:
|
|
88
|
+
ads_config["login_customer_id"] = login_customer_id
|
|
89
|
+
|
|
90
|
+
# Try to use OAuth token if available
|
|
91
|
+
token_path = os.path.expanduser("~/.config/claude-seo/oauth-token.json")
|
|
92
|
+
if os.path.exists(token_path):
|
|
93
|
+
with open(token_path) as f:
|
|
94
|
+
token_data = json.load(f)
|
|
95
|
+
if oauth_client_path:
|
|
96
|
+
with open(os.path.expanduser(oauth_client_path)) as f:
|
|
97
|
+
client_data = json.load(f)
|
|
98
|
+
client_info = client_data.get("web", client_data.get("installed", {}))
|
|
99
|
+
ads_config["client_id"] = client_info.get("client_id")
|
|
100
|
+
ads_config["client_secret"] = client_info.get("client_secret")
|
|
101
|
+
ads_config["refresh_token"] = token_data.get("refresh_token")
|
|
102
|
+
|
|
103
|
+
client = GoogleAdsClient.load_from_dict(ads_config)
|
|
104
|
+
return client, customer_id
|
|
105
|
+
|
|
106
|
+
except Exception as e:
|
|
107
|
+
print(f"Error building Google Ads client: {e}", file=sys.stderr)
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def generate_keyword_ideas(
|
|
112
|
+
seed_keywords: list,
|
|
113
|
+
language_id: str = "1000",
|
|
114
|
+
location_id: str = "2840",
|
|
115
|
+
limit: int = 50,
|
|
116
|
+
) -> dict:
|
|
117
|
+
"""
|
|
118
|
+
Generate keyword ideas from seed keywords.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
seed_keywords: List of seed keyword strings.
|
|
122
|
+
language_id: Language ID (1000 = English).
|
|
123
|
+
location_id: Location ID (2840 = United States).
|
|
124
|
+
limit: Max results.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Dictionary with keyword ideas and metrics.
|
|
128
|
+
"""
|
|
129
|
+
result = {
|
|
130
|
+
"seed_keywords": seed_keywords,
|
|
131
|
+
"ideas": [],
|
|
132
|
+
"error": None,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
client_data = _build_ads_client()
|
|
136
|
+
if not client_data:
|
|
137
|
+
result["error"] = "Could not build Google Ads client. Check config."
|
|
138
|
+
return result
|
|
139
|
+
|
|
140
|
+
client, customer_id = client_data
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
kp_service = client.get_service("KeywordPlanIdeaService")
|
|
144
|
+
request = client.get_type("GenerateKeywordIdeasRequest")
|
|
145
|
+
request.customer_id = customer_id
|
|
146
|
+
request.language = f"languageConstants/{language_id}"
|
|
147
|
+
request.geo_target_constants.append(f"geoTargetConstants/{location_id}")
|
|
148
|
+
request.keyword_plan_network = client.enums.KeywordPlanNetworkEnum.GOOGLE_SEARCH
|
|
149
|
+
request.keyword_seed.keywords.extend(seed_keywords)
|
|
150
|
+
|
|
151
|
+
response = kp_service.generate_keyword_ideas(request=request)
|
|
152
|
+
|
|
153
|
+
for idea in response.results:
|
|
154
|
+
metrics = idea.keyword_idea_metrics
|
|
155
|
+
monthly_volumes = []
|
|
156
|
+
for mv in metrics.monthly_search_volumes:
|
|
157
|
+
monthly_volumes.append({
|
|
158
|
+
"year": mv.year,
|
|
159
|
+
"month": mv.month,
|
|
160
|
+
"volume": mv.monthly_searches,
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
result["ideas"].append({
|
|
164
|
+
"keyword": idea.text,
|
|
165
|
+
"avg_monthly_searches": metrics.avg_monthly_searches,
|
|
166
|
+
"competition": metrics.competition.name if metrics.competition else "UNSPECIFIED",
|
|
167
|
+
"competition_index": metrics.competition_index,
|
|
168
|
+
"low_top_of_page_bid": metrics.low_top_of_page_bid_micros / 1_000_000 if metrics.low_top_of_page_bid_micros else None,
|
|
169
|
+
"high_top_of_page_bid": metrics.high_top_of_page_bid_micros / 1_000_000 if metrics.high_top_of_page_bid_micros else None,
|
|
170
|
+
"monthly_volumes": monthly_volumes[-12:] if monthly_volumes else [],
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
if len(result["ideas"]) >= limit:
|
|
174
|
+
break
|
|
175
|
+
|
|
176
|
+
# Sort by volume descending
|
|
177
|
+
result["ideas"].sort(key=lambda k: k.get("avg_monthly_searches", 0) or 0, reverse=True)
|
|
178
|
+
|
|
179
|
+
except GoogleAdsException as e:
|
|
180
|
+
errors = [err.message for err in e.failure.errors]
|
|
181
|
+
result["error"] = f"Google Ads API error: {'; '.join(errors)}"
|
|
182
|
+
except Exception as e:
|
|
183
|
+
result["error"] = f"Keyword Planner error: {e}"
|
|
184
|
+
|
|
185
|
+
return result
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def get_keyword_volumes(
|
|
189
|
+
keywords: list,
|
|
190
|
+
language_id: str = "1000",
|
|
191
|
+
location_id: str = "2840",
|
|
192
|
+
) -> dict:
|
|
193
|
+
"""
|
|
194
|
+
Get search volume for specific keywords.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
keywords: List of keywords to check.
|
|
198
|
+
language_id: Language ID.
|
|
199
|
+
location_id: Location ID.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Dictionary with keyword metrics.
|
|
203
|
+
"""
|
|
204
|
+
result = {
|
|
205
|
+
"keywords": [],
|
|
206
|
+
"error": None,
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
client_data = _build_ads_client()
|
|
210
|
+
if not client_data:
|
|
211
|
+
result["error"] = "Could not build Google Ads client."
|
|
212
|
+
return result
|
|
213
|
+
|
|
214
|
+
client, customer_id = client_data
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
kp_service = client.get_service("KeywordPlanIdeaService")
|
|
218
|
+
request = client.get_type("GenerateKeywordHistoricalMetricsRequest")
|
|
219
|
+
request.customer_id = customer_id
|
|
220
|
+
request.keywords.extend(keywords)
|
|
221
|
+
request.language = f"languageConstants/{language_id}"
|
|
222
|
+
request.geo_target_constants.append(f"geoTargetConstants/{location_id}")
|
|
223
|
+
request.keyword_plan_network = client.enums.KeywordPlanNetworkEnum.GOOGLE_SEARCH
|
|
224
|
+
|
|
225
|
+
response = kp_service.generate_keyword_historical_metrics(request=request)
|
|
226
|
+
|
|
227
|
+
for kw_result in response.results:
|
|
228
|
+
metrics = kw_result.keyword_metrics
|
|
229
|
+
result["keywords"].append({
|
|
230
|
+
"keyword": kw_result.text,
|
|
231
|
+
"avg_monthly_searches": metrics.avg_monthly_searches,
|
|
232
|
+
"competition": metrics.competition.name if metrics.competition else "UNSPECIFIED",
|
|
233
|
+
"competition_index": metrics.competition_index,
|
|
234
|
+
"low_top_of_page_bid": metrics.low_top_of_page_bid_micros / 1_000_000 if metrics.low_top_of_page_bid_micros else None,
|
|
235
|
+
"high_top_of_page_bid": metrics.high_top_of_page_bid_micros / 1_000_000 if metrics.high_top_of_page_bid_micros else None,
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
except GoogleAdsException as e:
|
|
239
|
+
errors = [err.message for err in e.failure.errors]
|
|
240
|
+
result["error"] = f"Google Ads API error: {'; '.join(errors)}"
|
|
241
|
+
except Exception as e:
|
|
242
|
+
result["error"] = f"Keyword volume error: {e}"
|
|
243
|
+
|
|
244
|
+
return result
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def main():
|
|
248
|
+
parser = argparse.ArgumentParser(
|
|
249
|
+
description="Google Ads Keyword Planner - SEO keyword research"
|
|
250
|
+
)
|
|
251
|
+
parser.add_argument(
|
|
252
|
+
"command",
|
|
253
|
+
choices=["ideas", "volume"],
|
|
254
|
+
help="Command: ideas (keyword suggestions), volume (search volume lookup)",
|
|
255
|
+
)
|
|
256
|
+
parser.add_argument("keywords", help="Seed keyword(s), comma-separated for volume")
|
|
257
|
+
parser.add_argument("--limit", type=int, default=50, help="Max results for ideas (default: 50)")
|
|
258
|
+
parser.add_argument("--language", default="1000", help="Language ID (default: 1000 = English)")
|
|
259
|
+
parser.add_argument("--location", default="2840", help="Location ID (default: 2840 = US)")
|
|
260
|
+
parser.add_argument("--json", "-j", action="store_true", help="Output as JSON")
|
|
261
|
+
|
|
262
|
+
args = parser.parse_args()
|
|
263
|
+
|
|
264
|
+
if args.command == "ideas":
|
|
265
|
+
seeds = [k.strip() for k in args.keywords.split(",")]
|
|
266
|
+
result = generate_keyword_ideas(seeds, language_id=args.language, location_id=args.location, limit=args.limit)
|
|
267
|
+
elif args.command == "volume":
|
|
268
|
+
kws = [k.strip() for k in args.keywords.split(",")]
|
|
269
|
+
result = get_keyword_volumes(kws, language_id=args.language, location_id=args.location)
|
|
270
|
+
|
|
271
|
+
if result.get("error"):
|
|
272
|
+
print(f"Error: {result['error']}", file=sys.stderr)
|
|
273
|
+
if not args.json:
|
|
274
|
+
sys.exit(1)
|
|
275
|
+
|
|
276
|
+
if args.json:
|
|
277
|
+
print(json.dumps(result, indent=2, default=str))
|
|
278
|
+
else:
|
|
279
|
+
if args.command == "ideas":
|
|
280
|
+
print(f"=== Keyword Ideas ===")
|
|
281
|
+
for i, idea in enumerate(result.get("ideas", [])[:20], 1):
|
|
282
|
+
vol = idea.get("avg_monthly_searches", "?")
|
|
283
|
+
comp = idea.get("competition", "?")
|
|
284
|
+
bid_low = idea.get("low_top_of_page_bid")
|
|
285
|
+
bid_high = idea.get("high_top_of_page_bid")
|
|
286
|
+
bid_str = f"${bid_low:.2f}-${bid_high:.2f}" if bid_low and bid_high else "N/A"
|
|
287
|
+
print(f" {i:2d}. {idea['keyword']:40s} | Vol: {vol:>8} | Comp: {comp:8s} | CPC: {bid_str}")
|
|
288
|
+
elif args.command == "volume":
|
|
289
|
+
print(f"=== Keyword Volumes ===")
|
|
290
|
+
for kw in result.get("keywords", []):
|
|
291
|
+
vol = kw.get("avg_monthly_searches", "?")
|
|
292
|
+
comp = kw.get("competition", "?")
|
|
293
|
+
print(f" {kw['keyword']:40s} | Vol: {vol:>8} | Comp: {comp}")
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
if __name__ == "__main__":
|
|
297
|
+
main()
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Google Cloud Natural Language API - Entity, sentiment, and content analysis.
|
|
4
|
+
|
|
5
|
+
Enhances E-E-A-T scoring with NLP entity coverage, sentiment analysis,
|
|
6
|
+
and Google's own content classification taxonomy.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python nlp_analyze.py --text "Your content here" --json
|
|
10
|
+
python nlp_analyze.py --url https://example.com --json
|
|
11
|
+
python nlp_analyze.py --text "Your content" --features entities,sentiment,classify
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import json
|
|
16
|
+
import sys
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
import requests
|
|
21
|
+
except ImportError:
|
|
22
|
+
print("Error: requests library required. Install with: pip install requests", file=sys.stderr)
|
|
23
|
+
sys.exit(1)
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
from google_auth import get_api_key, validate_url
|
|
27
|
+
except ImportError:
|
|
28
|
+
import os
|
|
29
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
30
|
+
from google_auth import get_api_key, validate_url
|
|
31
|
+
|
|
32
|
+
NLP_ENDPOINT = "https://language.googleapis.com/v2/documents:annotateText"
|
|
33
|
+
|
|
34
|
+
# Free tier: 5,000 units/month per feature
|
|
35
|
+
# Paid: $0.001 per 1,000-character unit for entity/sentiment
|
|
36
|
+
FEATURES = {
|
|
37
|
+
"entities": "extractEntities",
|
|
38
|
+
"sentiment": "extractDocumentSentiment",
|
|
39
|
+
"classify": "classifyText",
|
|
40
|
+
"categories": "classifyText",
|
|
41
|
+
"moderate": "moderateText",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def analyze_text(
|
|
46
|
+
text: str,
|
|
47
|
+
features: Optional[list] = None,
|
|
48
|
+
api_key: Optional[str] = None,
|
|
49
|
+
language: str = "en",
|
|
50
|
+
) -> dict:
|
|
51
|
+
"""
|
|
52
|
+
Analyze text using Google Cloud Natural Language API.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
text: Text content to analyze (max 1M characters).
|
|
56
|
+
features: List of features: entities, sentiment, classify, moderate.
|
|
57
|
+
api_key: Google API key.
|
|
58
|
+
language: Language code (default: en).
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Dictionary with entities, sentiment, categories, and moderation results.
|
|
62
|
+
"""
|
|
63
|
+
result = {
|
|
64
|
+
"text_length": len(text),
|
|
65
|
+
"language": language,
|
|
66
|
+
"entities": [],
|
|
67
|
+
"sentiment": None,
|
|
68
|
+
"categories": [],
|
|
69
|
+
"moderation": [],
|
|
70
|
+
"error": None,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
key = api_key or get_api_key()
|
|
74
|
+
if not key:
|
|
75
|
+
result["error"] = "No API key. Set GOOGLE_API_KEY or add 'api_key' to config."
|
|
76
|
+
return result
|
|
77
|
+
|
|
78
|
+
if features is None:
|
|
79
|
+
features = ["entities", "sentiment", "classify"]
|
|
80
|
+
|
|
81
|
+
# Build request
|
|
82
|
+
feature_map = {}
|
|
83
|
+
for f in features:
|
|
84
|
+
api_feature = FEATURES.get(f)
|
|
85
|
+
if api_feature:
|
|
86
|
+
feature_map[api_feature] = True
|
|
87
|
+
|
|
88
|
+
body = {
|
|
89
|
+
"document": {
|
|
90
|
+
"type": "PLAIN_TEXT",
|
|
91
|
+
"content": text[:100000], # API limit
|
|
92
|
+
"languageCode": language,
|
|
93
|
+
},
|
|
94
|
+
"features": feature_map,
|
|
95
|
+
"encodingType": "UTF8",
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
resp = requests.post(
|
|
100
|
+
f"{NLP_ENDPOINT}?key={key}",
|
|
101
|
+
json=body,
|
|
102
|
+
timeout=30,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
if resp.status_code == 403:
|
|
106
|
+
result["error"] = (
|
|
107
|
+
"Cloud Natural Language API access denied. Enable it in "
|
|
108
|
+
"GCP Console: APIs & Services > Library > Cloud Natural Language API. "
|
|
109
|
+
"Billing must be enabled on the project."
|
|
110
|
+
)
|
|
111
|
+
return result
|
|
112
|
+
|
|
113
|
+
if resp.status_code == 429:
|
|
114
|
+
result["error"] = "NLP API quota exceeded. Free tier: 5,000 units/month."
|
|
115
|
+
return result
|
|
116
|
+
|
|
117
|
+
resp.raise_for_status()
|
|
118
|
+
data = resp.json()
|
|
119
|
+
except requests.exceptions.RequestException as e:
|
|
120
|
+
result["error"] = f"NLP API request failed: {e}"
|
|
121
|
+
return result
|
|
122
|
+
|
|
123
|
+
# Entities
|
|
124
|
+
for entity in data.get("entities", []):
|
|
125
|
+
mentions = entity.get("mentions", [])
|
|
126
|
+
result["entities"].append({
|
|
127
|
+
"name": entity.get("name", ""),
|
|
128
|
+
"type": entity.get("type", "UNKNOWN"),
|
|
129
|
+
"salience": round(entity.get("salience", 0), 4),
|
|
130
|
+
"sentiment_score": entity.get("sentiment", {}).get("score"),
|
|
131
|
+
"sentiment_magnitude": entity.get("sentiment", {}).get("magnitude"),
|
|
132
|
+
"mention_count": len(mentions),
|
|
133
|
+
"metadata": entity.get("metadata", {}),
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
# Sort by salience (most important first)
|
|
137
|
+
result["entities"].sort(key=lambda e: e["salience"], reverse=True)
|
|
138
|
+
|
|
139
|
+
# Document sentiment
|
|
140
|
+
doc_sentiment = data.get("documentSentiment", {})
|
|
141
|
+
if doc_sentiment:
|
|
142
|
+
score = doc_sentiment.get("score", 0)
|
|
143
|
+
magnitude = doc_sentiment.get("magnitude", 0)
|
|
144
|
+
if score > 0.25:
|
|
145
|
+
tone = "positive"
|
|
146
|
+
elif score < -0.25:
|
|
147
|
+
tone = "negative"
|
|
148
|
+
else:
|
|
149
|
+
tone = "neutral"
|
|
150
|
+
|
|
151
|
+
result["sentiment"] = {
|
|
152
|
+
"score": round(score, 3),
|
|
153
|
+
"magnitude": round(magnitude, 3),
|
|
154
|
+
"tone": tone,
|
|
155
|
+
"interpretation": (
|
|
156
|
+
f"{'Positive' if score > 0 else 'Negative' if score < 0 else 'Neutral'} "
|
|
157
|
+
f"(score: {score:.2f}) with "
|
|
158
|
+
f"{'high' if magnitude > 2 else 'moderate' if magnitude > 0.5 else 'low'} "
|
|
159
|
+
f"emotional content (magnitude: {magnitude:.2f})"
|
|
160
|
+
),
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
# Sentence-level sentiment
|
|
164
|
+
sentences = data.get("sentences", [])
|
|
165
|
+
if sentences:
|
|
166
|
+
result["sentiment"]["sentence_count"] = len(sentences)
|
|
167
|
+
sent_scores = [s.get("sentiment", {}).get("score", 0) for s in sentences]
|
|
168
|
+
result["sentiment"]["most_positive"] = max(sent_scores) if sent_scores else 0
|
|
169
|
+
result["sentiment"]["most_negative"] = min(sent_scores) if sent_scores else 0
|
|
170
|
+
|
|
171
|
+
# Categories (content classification)
|
|
172
|
+
for cat in data.get("categories", []):
|
|
173
|
+
result["categories"].append({
|
|
174
|
+
"name": cat.get("name", ""),
|
|
175
|
+
"confidence": round(cat.get("confidence", 0), 4),
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
# Moderation categories
|
|
179
|
+
for mod in data.get("moderationCategories", []):
|
|
180
|
+
if mod.get("confidence", 0) > 0.5:
|
|
181
|
+
result["moderation"].append({
|
|
182
|
+
"name": mod.get("name", ""),
|
|
183
|
+
"confidence": round(mod.get("confidence", 0), 4),
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
return result
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def analyze_url(
|
|
190
|
+
url: str,
|
|
191
|
+
features: Optional[list] = None,
|
|
192
|
+
api_key: Optional[str] = None,
|
|
193
|
+
) -> dict:
|
|
194
|
+
"""
|
|
195
|
+
Fetch a URL's text content and analyze it.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
url: URL to fetch and analyze.
|
|
199
|
+
features: NLP features to extract.
|
|
200
|
+
api_key: API key override.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
Dictionary with NLP analysis results.
|
|
204
|
+
"""
|
|
205
|
+
if not validate_url(url):
|
|
206
|
+
return {"error": "Invalid URL. Only http/https URLs to public hosts are accepted."}
|
|
207
|
+
|
|
208
|
+
# Fetch the page text
|
|
209
|
+
try:
|
|
210
|
+
resp = requests.get(url, timeout=30, headers={
|
|
211
|
+
"User-Agent": "Mozilla/5.0 (compatible; ClaudeSEO/1.7 NLP Analyzer)"
|
|
212
|
+
})
|
|
213
|
+
resp.raise_for_status()
|
|
214
|
+
html = resp.text
|
|
215
|
+
except requests.exceptions.RequestException as e:
|
|
216
|
+
return {"error": f"Could not fetch URL: {e}"}
|
|
217
|
+
|
|
218
|
+
# Extract text from HTML (simple approach)
|
|
219
|
+
try:
|
|
220
|
+
from bs4 import BeautifulSoup
|
|
221
|
+
soup = BeautifulSoup(html, "html.parser")
|
|
222
|
+
# Remove script and style
|
|
223
|
+
for tag in soup(["script", "style", "nav", "footer", "header"]):
|
|
224
|
+
tag.decompose()
|
|
225
|
+
text = soup.get_text(separator=" ", strip=True)
|
|
226
|
+
except ImportError:
|
|
227
|
+
# Fallback: regex-based text extraction
|
|
228
|
+
import re
|
|
229
|
+
text = re.sub(r"<script[^>]*>.*?</script>", "", html, flags=re.DOTALL | re.IGNORECASE)
|
|
230
|
+
text = re.sub(r"<style[^>]*>.*?</style>", "", text, flags=re.DOTALL | re.IGNORECASE)
|
|
231
|
+
text = re.sub(r"<[^>]+>", " ", text)
|
|
232
|
+
text = re.sub(r"\s+", " ", text).strip()
|
|
233
|
+
|
|
234
|
+
if not text or len(text) < 50:
|
|
235
|
+
return {"error": "Extracted text too short for meaningful NLP analysis."}
|
|
236
|
+
|
|
237
|
+
result = analyze_text(text, features=features, api_key=api_key)
|
|
238
|
+
result["source_url"] = url
|
|
239
|
+
result["extracted_text_length"] = len(text)
|
|
240
|
+
return result
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def main():
|
|
244
|
+
parser = argparse.ArgumentParser(
|
|
245
|
+
description="Google Cloud Natural Language API - Entity/sentiment/classification for SEO"
|
|
246
|
+
)
|
|
247
|
+
parser.add_argument("--text", "-t", help="Text to analyze")
|
|
248
|
+
parser.add_argument("--url", "-u", help="URL to fetch and analyze")
|
|
249
|
+
parser.add_argument(
|
|
250
|
+
"--features", "-f",
|
|
251
|
+
default="entities,sentiment,classify",
|
|
252
|
+
help="Comma-separated features: entities, sentiment, classify, moderate (default: entities,sentiment,classify)",
|
|
253
|
+
)
|
|
254
|
+
parser.add_argument("--api-key", help="API key override")
|
|
255
|
+
parser.add_argument("--json", "-j", action="store_true", help="Output as JSON")
|
|
256
|
+
|
|
257
|
+
args = parser.parse_args()
|
|
258
|
+
|
|
259
|
+
if not args.text and not args.url:
|
|
260
|
+
print("Error: Provide --text or --url to analyze.", file=sys.stderr)
|
|
261
|
+
sys.exit(1)
|
|
262
|
+
|
|
263
|
+
features = [f.strip() for f in args.features.split(",")]
|
|
264
|
+
|
|
265
|
+
if args.url:
|
|
266
|
+
result = analyze_url(args.url, features=features, api_key=args.api_key)
|
|
267
|
+
else:
|
|
268
|
+
result = analyze_text(args.text, features=features, api_key=args.api_key)
|
|
269
|
+
|
|
270
|
+
if result.get("error"):
|
|
271
|
+
print(f"Error: {result['error']}", file=sys.stderr)
|
|
272
|
+
if not args.json:
|
|
273
|
+
sys.exit(1)
|
|
274
|
+
|
|
275
|
+
if args.json:
|
|
276
|
+
print(json.dumps(result, indent=2))
|
|
277
|
+
else:
|
|
278
|
+
if result.get("source_url"):
|
|
279
|
+
print(f"=== NLP Analysis: {result['source_url']} ===")
|
|
280
|
+
print(f"Text extracted: {result.get('extracted_text_length', 0):,} chars")
|
|
281
|
+
else:
|
|
282
|
+
print(f"=== NLP Analysis ({result.get('text_length', 0):,} chars) ===")
|
|
283
|
+
|
|
284
|
+
sent = result.get("sentiment")
|
|
285
|
+
if sent:
|
|
286
|
+
print(f"\nSentiment: {sent['tone'].upper()} (score: {sent['score']}, magnitude: {sent['magnitude']})")
|
|
287
|
+
print(f" {sent['interpretation']}")
|
|
288
|
+
|
|
289
|
+
entities = result.get("entities", [])
|
|
290
|
+
if entities:
|
|
291
|
+
print(f"\nTop Entities ({len(entities)} total):")
|
|
292
|
+
for e in entities[:15]:
|
|
293
|
+
print(f" [{e['type']:12s}] {e['name']} (salience: {e['salience']:.3f})")
|
|
294
|
+
|
|
295
|
+
categories = result.get("categories", [])
|
|
296
|
+
if categories:
|
|
297
|
+
print(f"\nContent Categories:")
|
|
298
|
+
for c in categories:
|
|
299
|
+
print(f" {c['name']} ({c['confidence']:.1%})")
|
|
300
|
+
|
|
301
|
+
moderation = result.get("moderation", [])
|
|
302
|
+
if moderation:
|
|
303
|
+
print(f"\nModeration Flags:")
|
|
304
|
+
for m in moderation:
|
|
305
|
+
print(f" {m['name']} ({m['confidence']:.1%})")
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
if __name__ == "__main__":
|
|
309
|
+
main()
|