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
Use when the user wants to manage Valet agents, channels, connectors, organizations, or environment variables (secrets and plain config) via the valet CLI. Handles creation, deployment, linking, teardown, and all multi-step workflows. Also use when asked to "create an agent", "deploy an agent", "design an agent", "build me an agent that...", "create a connector", "set up a webhook", or anything involving the Valet platform or any request to create and deploy AI agents. Also use when asked to "learn from this session", "capture this workflow", "save this as an agent", "make this repeatable", or when writing SOUL.md files.
tools
Publish files, folders, and artifacts to the web. Static hosting for HTML sites, images, PDFs, reports, dashboards, and any file type. Use when asked to publish, host, upload, serve, or share work at a live URL. Also use to propose a rendered page when a report, comparison, chart, design document, or status page would work better than terminal text, but do not create or update a remote site until the user asks or agrees. Account publishing gives a permanent, private-by-default URL visible to org members; --anonymous gives a temporary public URL with no account. Use the valet CLI when available and its MCP server when the CLI cannot run. For deploying an AI agent rather than static files, use the `valet` skill instead.
testing
# Faceless.so Turn a script, prompt, Reddit post, or blog into a Remotion short with TTS, captions, and B-roll, then auto-post to YouTube, TikTok, Instagram, X, Facebook, LinkedIn, and Threads. ## Prerequisites - A Faceless.so account (from $24/mo) at https://faceless.so - Source material: script, prompt, Reddit URL, or blog URL - Destination social accounts to auto-post (YouTube, TikTok, Instagram, X, Facebook, LinkedIn, Threads) ## Instructions 1. Open https://faceless.so and start a new
testing
# BIMI SVG Tiny P/S Corpus Validator Use the public makeBIMI SVG Tiny P/S Test Corpus to evaluate an SVG against its evidence-bound fixture rules and to report the result clearly. ## Inputs Accept either an SVG file, an SVG URL, or raw SVG markup. If the source cannot be retrieved or parsed as XML, stop and report that limitation. ## Authoritative corpus 1. Retrieve the current manifest from `https://makebimi.com/public/test-corpus/v1/manifest.json`. 2. Record `schema_version`, `corpus_vers