DEV Community

Cover image for Pipe Search Console Into Claude: A Small Node Script That Writes Your Weekly SEO Brief

Pipe Search Console Into Claude: A Small Node Script That Writes Your Weekly SEO Brief

Most developers who own a marketing site check Search Console the same way: open it, click around for ten minutes, close it, forget what you saw. The data is good. The habit is bad. The fix is a script that pulls the last few weeks of numbers, compares the most recent window to the one before it, and hands the deltas to a model that writes three paragraphs you will actually read on Monday morning.

Here is the whole thing. It runs on a cron, costs very little, and has nothing to do with dashboards.

What the script does

  1. Query the Search Console API for the last 28 days of clicks and impressions, split by query and by page.
  2. Split that into two 14 day windows and compute the change per query and per page.
  3. Sort the biggest movers in both directions.
  4. Send the movers to Claude with a short, strict prompt.
  5. Put the result wherever you read things: Slack, email, a Notion page, a text file.

The model is not analyzing the raw data. It is turning a sorted list of changes into sentences. That distinction keeps the output grounded and keeps the token bill small.

Why two windows instead of a plain weekly total? A total tells you the site got 412 clicks. It does not tell you that one page gained 90 and another lost 110, which is the only part worth your attention. Comparing like for like periods surfaces movement, and movement is what you can act on.

Setup

You need a Google Cloud service account with the Search Console API enabled, added as a user on the property in Search Console (Settings, then Users and permissions; Full or Restricted both work for reads). Download the JSON key. Set three environment variables:

GOOGLE_APPLICATION_CREDENTIALS=./sc-key.json
SC_SITE_URL=sc-domain:example.com
ANTHROPIC_API_KEY=sk-ant-...
Enter fullscreen mode Exit fullscreen mode

Use the sc-domain: form if your property is a domain property, or the full https://www.example.com/ URL if it is a URL prefix property. Install two packages:

npm install googleapis @anthropic-ai/sdk
Enter fullscreen mode Exit fullscreen mode

Pulling the data

// gsc.js
import { google } from 'googleapis';

const auth = new google.auth.GoogleAuth({
  scopes: ['https://www.googleapis.com/auth/webmasters.readonly'],
});
const sc = google.searchconsole({ version: 'v1', auth });

export async function fetchRows(startDate, endDate, dimension) {
  const res = await sc.searchanalytics.query({
    siteUrl: process.env.SC_SITE_URL,
    requestBody: {
      startDate,
      endDate,
      dimensions: [dimension],
      rowLimit: 1000,
    },
  });
  return (res.data.rows || []).map((r) => ({
    key: r.keys[0],
    clicks: r.clicks,
    impressions: r.impressions,
    position: r.position,
  }));
}
Enter fullscreen mode Exit fullscreen mode

Search Console data lags by a couple of days, so do not end the window at today. End it three days back and nobody will chase a phantom drop.

If the first call returns a 403, the service account email has not been added to the property yet, or you added it to the wrong property type. The domain property and the URL prefix property are separate entries in Search Console with separate user lists. Run sc.sites.list() once and the response tells you exactly which properties the account can see.

Computing the movers

// compare.js
export function diff(current, previous, limit = 15) {
  const prev = new Map(previous.map((r) => [r.key, r]));
  const rows = current.map((r) => {
    const p = prev.get(r.key) || { clicks: 0, impressions: 0, position: null };
    return {
      key: r.key,
      clicks: r.clicks,
      clicksDelta: r.clicks - p.clicks,
      impressions: r.impressions,
      impressionsDelta: r.impressions - p.impressions,
      position: r.position.toFixed(1),
      prevPosition: p.position ? p.position.toFixed(1) : null,
    };
  });
  const up = [...rows].sort((a, b) => b.clicksDelta - a.clicksDelta).slice(0, limit);
  const down = [...rows].sort((a, b) => a.clicksDelta - b.clicksDelta).slice(0, limit);
  return { up, down };
}
Enter fullscreen mode Exit fullscreen mode

Pages that had traffic last period and none this period will not appear in current. If that matters to you, run the diff in both directions and flag keys that only exist in previous. For most small sites the top and bottom fifteen tell the story.

Writing the brief

// brief.js
import Anthropic from '@anthropic-ai/sdk';
import { fetchRows } from './gsc.js';
import { diff } from './compare.js';

const client = new Anthropic();

function iso(daysAgo) {
  const d = new Date();
  d.setDate(d.getDate() - daysAgo);
  return d.toISOString().slice(0, 10);
}

const [curQ, prevQ, curP, prevP] = await Promise.all([
  fetchRows(iso(16), iso(3), 'query'),
  fetchRows(iso(30), iso(17), 'query'),
  fetchRows(iso(16), iso(3), 'page'),
  fetchRows(iso(30), iso(17), 'page'),
]);

const data = { queries: diff(curQ, prevQ), pages: diff(curP, prevP) };

const prompt = `You are writing a weekly SEO brief for the owner of a small business website.
Use only the data below. Do not invent numbers. Do not speculate about causes you cannot see in the data.
Write three short paragraphs: what went up, what went down, and one thing worth checking this week.
Plain English, no bullet points, no headings.

${JSON.stringify(data, null, 2)}`;

const msg = await client.messages.create({
  model: process.env.CLAUDE_MODEL,
  max_tokens: 800,
  messages: [{ role: 'user', content: prompt }],
});

console.log(msg.content[0].text);
Enter fullscreen mode Exit fullscreen mode

Set CLAUDE_MODEL to the current Sonnet model id from the Anthropic docs. Sonnet is more than enough for this. The input is a few thousand tokens of JSON and the output is three paragraphs.

Scheduling it

A GitHub Actions workflow on a Monday cron is the cheapest option if the repo already lives on GitHub. Store the secrets in the repository settings, write the service account JSON to disk in a step, and send the output wherever you want it.

# .github/workflows/seo-brief.yml
name: Weekly SEO brief
on:
  schedule:
    - cron: '0 14 * * 1'
  workflow_dispatch:
jobs:
  brief:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npm ci
      - run: echo "$SC_KEY_JSON" > sc-key.json
        env: { SC_KEY_JSON: ${{ secrets.SC_KEY_JSON }} }
      - run: node brief.js > brief.md
        env:
          GOOGLE_APPLICATION_CREDENTIALS: ./sc-key.json
          SC_SITE_URL: ${{ secrets.SC_SITE_URL }}
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          CLAUDE_MODEL: ${{ vars.CLAUDE_MODEL }}
      - uses: actions/upload-artifact@v4
        with: { name: seo-brief, path: brief.md }
Enter fullscreen mode Exit fullscreen mode

Swap the artifact step for a Slack webhook or an email action once you trust the output. Keep workflow_dispatch in there so you can trigger a run by hand while you are tuning the prompt.

The prompt rules matter more than the model

Three constraints in that prompt do the real work. "Use only the data below" stops it from reaching for general SEO advice. "Do not speculate about causes" stops it from blaming an algorithm update it has no evidence for. "One thing worth checking" forces a single action instead of a to-do list nobody reads. Loosen those and the brief turns into content, and content is the thing you were trying to avoid reading.

Where this goes next

Once the brief lands every Monday, the same pattern extends. Pull GA4 landing page data next to the Search Console rows. Flag pages whose impressions rose while clicks did not, which usually points at the title tag. Diff against the same window last year for seasonal businesses. Each one is another fetch function and another field in the JSON. The model call does not change.

This is the smallest version of what we build when a business wants an in-house AI marketing system on Claude: plain code gathers the facts, the model writes the sentences, and a human reads three paragraphs instead of a dashboard.

Carlynn Espinoza runs Level Up Digital Marketing Group, a San Diego agency building SEO, paid media, and AI systems for service businesses.

Top comments (0)