packages/skills/skills/m13v-gsc-seo-page/SKILL.md
Generate SEO pages from Google Search Console queries, and onboard new products into the automated GSC/SERP pipeline. Delegates page generation to ~/social-autoposter/seo/generate_page.py and covers end-to-end product onboarding: GSC domain registration via API, config.json wiring, launchd activation, backfill, and verification.
npx skillsauth add mediar-ai/skillhubz gsc-seo-pageInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Two modes:
All page generation goes through a single canonical script:
~/social-autoposter/seo/generate_page.py
This script handles everything: SERP research, dynamic component discovery from @seo/components, theme detection, page writing, and build verification. Do NOT write pages manually or use inline TSX patterns; always run the generator.
cd ~/social-autoposter
python3 seo/generate_page.py \
--product fazm \
--keyword "your target query" \
--slug "your-target-query" \
--trigger manual
The generator will:
@seo/componentsThe cron pipeline (seo/run_gsc_pipeline.sh) handles:
seo/fetch_gsc_queries.pygsc_queries table)in_progressgenerate_page.py --trigger gscCron wrappers: seo/cron_gsc.sh (GSC queries) and seo/cron_seo.sh (SERP discovery).
All products in ~/social-autoposter/config.json: Fazm, Assrt, PieLine, Cyrano, etc.
Use this section when a new consumer site (already scaffolded with the setup-client-website skill and deployed at https://DOMAIN) needs to be plugged into the automated page-generation pipeline. Five steps: register the domain in GSC via API, add the product to config.json, activate the launchd jobs, backfill, verify.
What this unlocks once wired:
seo/cron_gsc.sh (every 10 min): pulls GSC queries for the domain, picks the highest-impression pending query, writes one guide page per runseo/cron_seo.sh (every 10 min): DataForSEO SERP discovery that proposes net-new keywords before they show in GSC~/CLIENT-website/src/app/(main)/t/{slug}/page.tsx, build locally, commit, push to main, and wait for deployPrerequisites:
Site deployed at https://DOMAIN with /t/ scaffolding and sitemap accessible at https://DOMAIN/sitemap.xml (run the setup-client-website skill first)
~/social-autoposter/.env defines these keys (the skill reads them at runtime; set once per workstation):
GSC_SA_KEY_PATH=~/social-autoposter/seo/credentials/<sa-key>.json
GSC_SA_EMAIL=<service-account>@<gcp-project>.iam.gserviceaccount.com
GSC_GCP_PROJECT=<gcp-project-id>
GSC_ADMIN_EMAIL=<email-that-owns-GCP-project>
CLOUD_DNS_PROJECT=<gcloud-dns-project> # only needed for Cloud DNS path; skip for Vercel DNS
DATABASE_URL=postgres://... # Neon, already set by social-autoposter init
The SA key file at $GSC_SA_KEY_PATH exists and has webmasters + siteverification scopes authorized
The client repo is cloned at the path you will write into landing_pages.repo
Source the env before running any of the Python snippets below:
set -a && source ~/social-autoposter/.env && set +a
The service account can register and verify a domain property end-to-end via the Site Verification API plus the Search Console API. No browser UI, no manual clicking. Once this completes, the SA is siteOwner on the property and fetch_gsc_queries.py can pull impression data immediately.
1a. Enable the Site Verification API in the SA's project (once per project; skip if already enabled on $GSC_GCP_PROJECT):
gcloud services enable siteverification.googleapis.com \
--project="$GSC_GCP_PROJECT" \
--account="$GSC_ADMIN_EMAIL"
The --account must be an owner of $GSC_GCP_PROJECT, not any gcloud-authenticated email.
1b. Get the DNS TXT verification token. Set DOMAIN to the bare domain (e.g. clientdomain.com, not https://clientdomain.com):
export DOMAIN=clientdomain.com
python3 -c "
import os
from google.oauth2 import service_account
from googleapiclient.discovery import build
creds = service_account.Credentials.from_service_account_file(
os.path.expanduser(os.environ['GSC_SA_KEY_PATH']),
scopes=['https://www.googleapis.com/auth/siteverification'],
)
sv = build('siteVerification', 'v1', credentials=creds)
res = sv.webResource().getToken(body={
'site': {'type': 'INET_DOMAIN', 'identifier': os.environ['DOMAIN']},
'verificationMethod': 'DNS_TXT',
}).execute()
print(res['token'])
"
1c. Add the TXT record.
For domains on Vercel DNS (personal-account Vercel domains):
vercel dns add "$DOMAIN" @ TXT "google-site-verification=TOKEN"
Do not pass --scope for personal-account domains. Passing a personal-scope flag fails with "You cannot set your Personal Account as the scope." Use --scope <team-slug> only for team-owned domains.
For domains on Google Cloud DNS (requires CLOUD_DNS_PROJECT in env):
gcloud dns record-sets transaction start --zone=ZONE_NAME --project="$CLOUD_DNS_PROJECT"
gcloud dns record-sets transaction add '"google-site-verification=TOKEN"' \
--name="${DOMAIN}." --ttl=300 --type=TXT --zone=ZONE_NAME --project="$CLOUD_DNS_PROJECT"
gcloud dns record-sets transaction execute --zone=ZONE_NAME --project="$CLOUD_DNS_PROJECT"
1d. Wait ~15 seconds for propagation, then confirm:
dig +short TXT "$DOMAIN" @8.8.8.8 | grep google-site-verification
If nothing shows, wait 30 seconds and retry.
1e. Complete verification, add to Search Console, and submit the sitemap in one shot:
python3 -c "
import os
from google.oauth2 import service_account
from googleapiclient.discovery import build
DOMAIN = os.environ['DOMAIN']
creds = service_account.Credentials.from_service_account_file(
os.path.expanduser(os.environ['GSC_SA_KEY_PATH']),
scopes=[
'https://www.googleapis.com/auth/siteverification',
'https://www.googleapis.com/auth/webmasters',
],
)
sv = build('siteVerification', 'v1', credentials=creds)
sv.webResource().insert(
verificationMethod='DNS_TXT',
body={'site': {'type': 'INET_DOMAIN', 'identifier': DOMAIN}, 'owners': []},
).execute()
sc = build('searchconsole', 'v1', credentials=creds)
sc.sites().add(siteUrl=f'sc-domain:{DOMAIN}').execute()
sc.sitemaps().submit(siteUrl=f'sc-domain:{DOMAIN}', feedpath=f'https://{DOMAIN}/sitemap.xml').execute()
print(f'OK: sc-domain:{DOMAIN} verified, added, sitemap submitted')
"
After this runs successfully, the SA is siteOwner, the sitemap is submitted, and GSC starts accumulating impression data (real data usually takes 1 to 3 days).
Why API not browser: no manual clicking, no browser lock contention, works from any machine that has the SA credentials. A browser flow is only needed if the domain belongs to a GCP project the SA is not enabled on, or if you have no access to the SA.
~/social-autoposter/config.jsonEdit ~/social-autoposter/config.json and add the client as a new entry under the projects[] array:
{
"name": "ClientName",
"weight": 10,
"landing_pages": {
"repo": "~/CLIENT-website",
"github_repo": "<GITHUB_ORG>/CLIENT-website",
"base_url": "https://clientdomain.com",
"gsc_property": "sc-domain:clientdomain.com",
"product_source": [
{
"path": "~/CLIENT-website",
"description": "1 to 2 sentence description of what this site is, who the client is, and what topics the guide pages should cover. Used in the LLM prompt when generating pages."
}
]
}
}
Field semantics (verified against seo/select_product.py and seo/generate_page.py):
| Field | Read by | Purpose |
|---|---|---|
| name | both crons | Product label used in logs, DB rows (gsc_queries.product, seo_keywords.product), and the dashboard |
| weight | both crons | Relative pick frequency in the weighted-random product selection. Default is 10. Smaller or less active products use 6. Setting to 0 disables without deleting |
| landing_pages.repo | seo/select_product.py:29 eligibility gate; seo/generate_page.py:147 | Path to the client's Next.js repo on disk. Must exist or the product is silently skipped |
| landing_pages.github_repo | optional, reference | Not read by the generator (git remote is read from the local .git/config). Keep for documentation |
| landing_pages.base_url | seo/generate_page.py:148 | Prepended to /t/{slug} for final URL used in verification and logs |
| landing_pages.gsc_property | seo/select_product.py:31 eligibility gate for cron_gsc.sh | Must match sc-domain:... exactly. Without it the product is invisible to the GSC cron |
| landing_pages.product_source[] | seo/generate_page.py:119 | Grounds generated content in real product details. Each entry has path (a README, docs dir, or source root) and description |
Without gsc_property set, cron_gsc.sh silently skips the product forever. Without repo on disk, both crons skip it.
Both SEO crons ship as plists in ~/social-autoposter/launchd/ but npx social-autoposter init does NOT auto-load them (only the social posting plists are auto-loaded). Link and load them once, on the machine that will run the pipelines:
ln -sf ~/social-autoposter/launchd/com.m13v.social-gsc-seo.plist ~/Library/LaunchAgents/
ln -sf ~/social-autoposter/launchd/com.m13v.social-serp-seo.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/com.m13v.social-gsc-seo.plist
launchctl load ~/Library/LaunchAgents/com.m13v.social-serp-seo.plist
Verify both are registered:
launchctl list | grep -E "social-(gsc|serp)-seo"
Expect two lines, each with a PID (when a run is currently executing) or - plus the last exit code. Both jobs fire every 600 seconds (10 min). Skip this entire step if launchctl list already shows both.
cron_gsc.sh does not fetch queries eagerly; the fetch happens lazily inside each run. If the domain was just registered and has zero rows in the gsc_queries table, the first cron picks will do nothing. To kickstart, run the fetch manually:
cd ~/social-autoposter
python3 seo/fetch_gsc_queries.py --product ClientName
Expect 0 queries for brand-new properties (GSC impression data takes 1 to 3 days to accumulate). Re-run the fetch after a few days to populate the table. Once pending queries with impressions >= 5 exist, the cron will start generating pages automatically without further action.
# 1. Confirm the product is eligible for both pipelines.
python3 ~/social-autoposter/seo/select_product.py --require-gsc
# Run it 20 times; the product's name should appear proportional to its weight.
# 2. Inspect pending queries after a fetch.
source ~/social-autoposter/.env
psql "$DATABASE_URL" -c "SELECT status, COUNT(*) FROM gsc_queries WHERE product = 'ClientName' GROUP BY status;"
# 3. Force one GSC run without waiting for the cron.
~/social-autoposter/seo/run_gsc_pipeline.sh ClientName
# Writes to ~/social-autoposter/seo/logs/gsc_ClientName.log
# Either creates a new page at ~/CLIENT-website/src/app/(main)/t/{slug}/page.tsx,
# or exits with "No pending queries with >= 5 impressions. Done."
# 4. Watch the cron pick the product organically.
tail -f ~/social-autoposter/skill/logs/cron_gsc-*.log
Onboarding checklist:
sc-domain:DOMAIN appears in the SA's site list (sc.sites().list() includes it with permissionLevel: "siteOwner")~/social-autoposter/config.json with all five required landing_pages fieldsselect_product.py --require-gsc includes the product in its rotationcom.m13v.social-gsc-seo and com.m13v.social-serp-seo appear in launchctl listrun_gsc_pipeline.sh ClientName completes without error (an exit of "no pending queries" is acceptable on day one)gsc_queries table shows rows with status='pending' and impressions >= 5 for the new productseo-page-ui blueprint (deleted; it produced plain pages without components)@seo/componentsunderserved-seo-page skill (deleted; replaced by run_serp_pipeline.sh)| What | Where |
|---|---|
| Generator | ~/social-autoposter/seo/generate_page.py |
| GSC pipeline | ~/social-autoposter/seo/run_gsc_pipeline.sh |
| SERP pipeline | ~/social-autoposter/seo/run_serp_pipeline.sh |
| GSC cron wrapper | ~/social-autoposter/seo/cron_gsc.sh |
| SERP cron wrapper | ~/social-autoposter/seo/cron_seo.sh |
| Product selector | ~/social-autoposter/seo/select_product.py |
| GSC fetch | ~/social-autoposter/seo/fetch_gsc_queries.py |
| Postgres state | gsc_queries and seo_keywords tables |
| Component library | ~/seo-components/src/index.ts (@seo/components) |
| Product config | ~/social-autoposter/config.json |
| launchd plists | ~/social-autoposter/launchd/com.m13v.social-{gsc,serp}-seo.plist |
tools
Design web-like user interfaces in the terminal and inside tmux with a cell-grid Canvas, CSS-like box model, flexbox/grid layout, and 15 reusable widgets such as Panel, Table, Card, ProgressBar, Meter, Tabs, Tree, Badge, Banner, and a braille line chart. Use when an agent needs a dashboard, panel, table, status page, TUI layout, tmux dashboard, screenshot-driven CLI/TUI replica, ANSI frame, truecolor render, pyte PNG screenshot smoke test, wide-character alignment, or a new terminal widget.
tools
Drive interactive terminal (TUI) programs — CLIs, REPLs, installers, menu apps, agent CLIs, and editors like vim — through a PTY, reading semantic screen snapshots. A pattern library classifies a screen (REPL, menu, pager, fzf search, confirm dialog, form, spinner, wizard) and drives it with a ready recipe. Use when a program expects a live terminal (arrow-key menus, prompts, spinners, password fields, curses UIs), or when a piped command hangs or prints nothing.
tools
Design and render terminal/CMD visual effects and ASCII art from a one-line request via the pluggable `fx` engine (18 hot-swappable, themeable effects plus scripted shows). Effects include donut, matrix rain, plasma, fire, a spinning 3D ball, Game of Life, wireframe cube, 3D text banners, rainbow/lolcat gradient text, starfield, tunnel, fireworks, image-to-ASCII, and more. Use when the request is for a terminal animation, ANSI/CLI art, or a new console effect. Pure Python stdlib; truecolor.
tools
# X Twitter Scraper Use Xquik for X/Twitter tweet search, user lookup, profile tweets, follower export, media download, monitors, webhooks, posting workflows, and MCP-backed API exploration. ## Prerequisites - A Xquik API key in `XQUIK_API_KEY`. - Internet access to `https://xquik.com/api/v1`, `https://xquik.com/mcp`, and `https://docs.xquik.com`. - A clear user request that identifies the target tweets, users, accounts, keywords, media, monitor, webhook, or write action. ## Source Truth -