| name | gsc-seo-page |
| description | 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. |
| user_invocable | true |
GSC SEO Page
Two modes:
- Generate a page for a query that GSC is already showing impressions for (the common case).
- Onboard a new product into the GSC + SERP pipelines so the crons start producing pages for a new domain automatically.
How It Works
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.
Usage
Manual (single page)
cd ~/social-autoposter
python3 seo/generate_page.py \
--product fazm \
--keyword "your target query" \
--slug "your-target-query" \
--trigger manual
The generator will:
- Research the SERP for the keyword
- Discover available components from
@seo/components
- Read the target repo's theme (dark/light) from its CSS
- Write a rich page with animated components, Magic UI elements, comparison tables, FAQ sections, etc.
- Build and verify locally
- Commit and push to main
- Wait for deployment and verify HTTP 200
Pipeline (automated, via cron)
The cron pipeline (seo/run_gsc_pipeline.sh) handles:
- Fetching queries from GSC via
seo/fetch_gsc_queries.py
- Picking the highest-impression pending query from Postgres (
gsc_queries table)
- Marking it
in_progress
- Calling
generate_page.py --trigger gsc
- Updating Postgres with the result
Cron wrappers: seo/cron_gsc.sh (GSC queries) and seo/cron_seo.sh (SERP discovery).
Supported products
All products in ~/social-autoposter/config.json: Fazm, Assrt, PieLine, Cyrano, etc.
Onboarding a New Product
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 run
seo/cron_seo.sh (every 10 min): DataForSEO SERP discovery that proposes net-new keywords before they show in GSC
- Both write TSX pages into
~/CLIENT-website/src/app/(main)/t/{slug}/page.tsx, build locally, commit, push to main, and wait for deploy
Prerequisites:
-
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
Step 1. Register the Domain in Google Search Console (API path, no browser)
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.
Step 2. Add the Product to ~/social-autoposter/config.json
Edit ~/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.
Step 3. Activate the SEO launchd Jobs
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.
Step 4. Backfill GSC Queries (optional but recommended)
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.
Step 5. Verify End-to-End
python3 ~/social-autoposter/seo/select_product.py --require-gsc
source ~/social-autoposter/.env
psql "$DATABASE_URL" -c "SELECT status, COUNT(*) FROM gsc_queries WHERE product = 'ClientName' GROUP BY status;"
~/social-autoposter/seo/run_gsc_pipeline.sh ClientName
tail -f ~/social-autoposter/skill/logs/cron_gsc-*.log
Onboarding checklist:
What NOT to do
- Do NOT write page TSX manually with inline SVGs and raw HTML tables
- Do NOT use the old
seo-page-ui blueprint (deleted; it produced plain pages without components)
- Do NOT create local guide component files in product repos; use
@seo/components
- Do NOT use the old
underserved-seo-page skill (deleted; replaced by run_serp_pipeline.sh)
- Do NOT register GSC properties through the browser UI when the SA can do it via API (Step 1 above)
Writing rules (apply to all generated content)
- No em dashes or en dashes anywhere
- No AI vocabulary: delve, crucial, robust, comprehensive, nuanced, multifaceted, furthermore, moreover, additionally, pivotal, landscape, tapestry, underscore, foster, showcase, intricate, vibrant, fundamental
- Real code only, not pseudocode
- Concrete numbers, not vague claims
- First-person plural or second person voice
Quick reference
| 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 |