? Developer Guide · BigCommerce + Claude AI

Build an AI-Powered
SEO Optimiser
from Scratch

A complete step-by-step walkthrough of how we built, debugged and enhanced a production-ready tool that generates SEO copy for an entire product catalogue — with auto-resume, crash recovery and batch processing.

? 12-minute read ? Video walkthrough coming soon ⚙️ React · Claude API · PapaParse

This guide documents the real development journey of a BigCommerce SEO Optimiser — built entirely inside Claude.ai without writing a single line of code manually. We started with a simple idea: "Can Claude generate page titles and meta descriptions for 1,557 products?" What followed was six rounds of enhancement, four bug fixes, and a complete architectural rewrite.

Each section maps to a real conversation exchange, so you can follow exactly what prompts were used, what broke, why it broke, and how it was fixed. By the end you'll be able to build your own version — or adapt the pattern for any bulk AI processing task.

If you want to save time you can download the file we have developed and enhance it using Claude to meet your own requirements. Download Now

What the finished tool does

Bulk AI Generation

Processes 1,500+ products in batches using the Claude API.

?

Crash Recovery

Progress saved to localStorage — survives logout and tab close.

?

Auto-Resume

Detects credit limit errors and resumes automatically when limits refresh.

?️

Safe CSV Output

PapaParse preserves all original data — only 4 SEO columns are touched.

Partial Export

Download a BigCommerce-ready CSV at any point during processing.

Pause & Resume

Manually pause at any time and pick up exactly where you left off.

1
Phase 1 · Foundation

Build the First Version

Video · Phase 1 — Building the initial app · Coming soon
1

Upload your product CSV to Claude

Export your product catalogue from BigCommerce (Products → Export → CSV). Upload it directly into the Claude.ai chat. Claude will read the file and identify the column structure — specifically Item, ID, Name, Description, Page Title, Meta Description, Search Keywords and Meta Keywords.

Prompt used
"Analyse data and improve SEO page title, meta description, search keywords, meta keywords for each product and provide CSV output file"
2

Ask Claude to build a React artifact

Claude reads the CSV, identifies 1,557 product rows and builds a React artifact — an interactive app running directly in the chat window. It reads the uploaded file client-side using PapaParse, calls the Claude API in batches of 5, and renders a live preview of generated SEO copy.

?
Why an artifact, not a script? A React artifact runs in the browser with no server required. The user uploads their CSV, the AI processes it, and they download the result — all in one tab. No infrastructure needed.
3

Define a strong system prompt for SEO quality

The quality of generated SEO copy depends entirely on the instructions given to the Claude API inside the artifact. The system prompt specifies exact character limits, format rules and tone for each field.

System prompt — inside the artifact
const SEO_SYSTEM_PROMPT = `
PAGE TITLE: 50-60 chars max. Keyword-rich, natural, readable.
META DESCRIPTION: 150-160 chars. Value prop + CTA.
  End with "Shop now" or "Free UK delivery".
SEARCH KEYWORDS: 8-12 comma-separated phrases.
META KEYWORDS: Same, semicolon-separated.

Respond ONLY with a valid JSON array:
[{"id":"112","pageTitle":"...","metaDescription":"..."}]
`;
⚠️
Always end the prompt with strict JSON instructions. Without "Respond ONLY with a valid JSON array", Claude may include markdown fences or preamble text that breaks your JSON.parse() call.
4

Test with a small batch first

Before running all 1,557 products, upload the CSV and click Start. Watch the first 3–4 batches complete and inspect the live preview panel. Check that page titles are under 60 characters and meta descriptions are 150–160 characters. Adjust the system prompt if the quality isn't right before committing to a full run.

2
Phase 2 · First Bug

Fix the JSON Truncation Error

Video · Phase 2 — Diagnosing and fixing the token limit error · Coming soon

The first run immediately produced this error for every single batch:

Error seen in the app
Batch IDs 112, 113, 114, 115, 116:
Unterminated string in JSON at position 3666
(line 33 column 101)
❌ Problem — 0 of 1,557 products optimised
The API was returning a valid response, but it was being cut off mid-JSON because max_tokens: 1000 was too low. A batch of 5 products with full SEO fields easily exceeds 1,000 tokens. The JSON was incomplete, so JSON.parse() threw a syntax error and the entire batch was skipped.
✅ Fix — Two changes made together
1. Increase max_tokens from 1,000 → 4,000 — gives the model enough room to complete all fields for a batch without truncating.

2. Reduce batch size from 5 → 2 products — smaller batches mean less output per call, reducing the chance of hitting the limit even at 4,000 tokens.

3. Add individual retry fallback — if a 2-product batch still fails to parse, retry each product individually before skipping.
The fix
// Before
const PRODUCTS_PER_BATCH = 5;
max_tokens: 1000

// After
const PRODUCTS_PER_BATCH = 2;
max_tokens: 4000

// Add retry logic
try {
  parsed = JSON.parse(clean);
} catch {
  // Retry each product individually
  for (const p of batch) {
    const r = await callAPI([p]);
    parsed.push(...r);
  }
}
max_tokens JSON truncation batch size retry logic
3
Phase 3 · Resilience

Persist Progress Across Sessions

Video · Phase 3 — Adding localStorage persistence · Coming soon

Processing 1,557 products takes time. If the tab is closed, the browser crashes, or the user logs out of Claude mid-run, all progress is lost. The solution is to save results to localStorage after every batch.

1

Save after every batch

After each successful batch, serialise the results object and write it to localStorage under a fixed key. This happens automatically with no user action required.

Save after each batch
const STORAGE_KEY = "bc_seo_progress";

// After processing each batch:
localStorage.setItem(STORAGE_KEY, JSON.stringify({
  savedResults:      current,
  savedFileName:     fileName,
  savedProductCount: products.length,
  savedAt:           new Date().toISOString(),
}));
2

Restore on page load

When the app loads, check localStorage for a previous session. If found, pre-populate the results and show a "Progress Restored" banner telling the user how many products were already done.

Restore on mount
useEffect(() => {
  const raw = localStorage.getItem(STORAGE_KEY);
  if (!raw) return;
  const { savedResults, savedFileName } = JSON.parse(raw);
  if (savedResults) {
    E.results = savedResults;
    setUiResults(savedResults);
    setUiStatus("restored");
  }
}, []);
3

Resume from the right position

When processing starts (or resumes), filter the product list to only those without a saved result. Since results are keyed by product ID, this automatically skips everything already done.

Skip already-processed products
const current = { ...E.results };
const todo = products.filter(p => !current[p.id]);
// Only processes products not yet in results
?
Don't close the tab during a credit-limit pause. localStorage persists across page refreshes but the tab must remain open while auto-resume is polling. The results are safe — it's just the timer that lives in memory.
4
Phase 4 · Data Safety

Fix the CSV Output Bug

Video · Phase 4 — Switching to PapaParse for safe CSV handling · Coming soon

After the first successful download, the user noticed that re-importing the CSV into BigCommerce added extra rows. The original product descriptions — which contain HTML bullet lists with embedded newlines — were being split across multiple rows.

Example description that caused the issue
// Original Description field (inside a quoted CSV cell):
"
  • Round necklace & stud earring set
  • Purple mother of pearl shell
  • Pendant 15mm high
"
// Custom JS parser incorrectly split this into 5 rows!
❌ Root cause — custom CSV parser
The app used a hand-written CSV parser that split on newline characters. It didn't handle the case where a newline appears inside a quoted field — which is perfectly valid in CSV but requires proper state-machine parsing to handle correctly.
✅ Fix — Switch to PapaParse for both reading and writing
PapaParse is a battle-tested CSV library available in React artifacts (import Papa from "papaparse"). It correctly handles quoted fields containing newlines, commas, and special characters — exactly what BigCommerce exports.

Crucially, use Papa.unparse() with quotes: true when writing the output CSV. This re-quotes every field, so newlines inside descriptions are preserved safely.
Before and after
// ❌ Before — custom parser, breaks on multiline fields
function parseCSV(text) {
  const lines = text.split('\n'); // WRONG
  ...
}

// ✅ After — PapaParse handles all edge cases
Papa.parse(file, {
  header: true,
  skipEmptyLines: false, // keep ALL rows
  complete: (parsed) => { ... }
});

// ✅ Write back with proper quoting
const csv = Papa.unparse({ fields, data: updated }, {
  quotes: true,   // always quote — safest for re-import
  newline: "\n",
});
?️
Only update the 4 SEO columns. When rebuilding the output rows, spread the original row first (...row) and only overwrite Page Title, Meta Description, Search Keywords, and Meta Keywords. Every other column — description, images, variants, pricing — passes through untouched.
5
Phase 5 · Smart Recovery

Add Auto-Resume After Credit Refresh

Video · Phase 5 — Building the polling and auto-resume system · Coming soon

Running 1,557 products through the Claude API in batches of 2 makes roughly 780 API calls. On a standard plan this will eventually hit a rate or credit limit. Instead of requiring the user to manually resume, the app should detect the error, wait, and resume automatically.

1

Detect credit errors vs other errors

When the API returns an error, inspect the message and HTTP status code to classify it. Credit/billing errors need polling; rate limit errors just need a short wait; other errors should skip the batch and continue.

Error classification
function isCreditError(msg, httpStatus) {
  return /credit|billing|payment|quota|insufficient/i
    .test(msg || '') || httpStatus === 402;
}
function isRateLimit(msg, httpStatus) {
  return /rate.limit|overloaded/i.test(msg || '')
    || httpStatus === 429 || httpStatus === 529;
}
2

Build a lightweight probe function

The probe sends a tiny request to the API — just "ping" with max_tokens of 10. If it gets any valid response back (even a different kind of error), billing is fine and we can resume. Only credit/billing errors should keep the polling going.

probeCredits() — critical detail
async function probeCredits() {
  try {
    const res = await fetch("https://api.anthropic.com/v1/messages", {
      method: "POST",
      body: JSON.stringify({
        model: "claude-sonnet-4-6", max_tokens: 10,
        system: "Reply: ok",
        messages: [{ role: "user", content: "ping" }]
      })
    });
    const data = await res.json();
    if (!data.error) return { ok: true }; // ✅ credits restored
    if (isCreditError(data.error.message, res.status))
      return { ok: false, reason: "credit" };
    return { ok: true }; // other API error = billing fine
  } catch {
    return { ok: false, reason: "network" };
  }
}
?
Common mistake: returning false for ALL errors in probeCredits. If you catch network errors and return false, the probe never returns true and auto-resume never fires. Only credit errors should return { ok: false }.
3

Run a countdown polling loop

When a credit error is detected, set a 60-second timer. Decrement a countdown counter every second for the UI. When the timer fires, call probeCredits(). If ok, call runLoop() directly. If not, reschedule.

schedulePoll() — the polling engine
E.schedulePoll = function(attempt) {
  let secs = 60;
  E.cdTimer = setInterval(() => {
    secs -= 1;
    setUiCountdown(secs);
  }, 1000);

  E.pollTimer = setTimeout(async () => {
    clearInterval(E.cdTimer);
    const probe = await probeCredits();
    if (probe.ok) {
      E.runLoop(); // ✅ resume immediately
    } else {
      E.schedulePoll(attempt + 1); // try again
    }
  }, 60000);
};
6
Phase 6 · Architecture Fix

Fix the Stale Closure Bug

Video · Phase 6 — The stale closure problem and the engine pattern · Coming soon

Auto-resume still didn't work. The polling probe succeeded, runLoop() was called — but nothing happened. This was the most subtle bug of the entire build.

❌ Root cause — React stale closures
The processing and polling functions were defined using useCallback hooks. In React, hooks capture the values of variables at the time they're created. When schedulePoll was defined, it captured the initial version of runProcessingLoop. By the time the probe succeeded and called runProcessingLoop(), it was calling a frozen copy that had no connection to the live app state — so nothing happened.
✅ Fix — The Engine Pattern
Move all processing logic out of React hooks entirely and into a plain JavaScript object (an "engine") created once using useRef. Plain object methods always read from the same shared mutable object — there are no closures, no stale captures, and no React rendering cycle involved.
The engine pattern — created once, lives forever
// Create the engine object once and store it in a ref
const engineRef = useRef(null);
if (!engineRef.current) {
  engineRef.current = createEngine(setters);
}
const E = engineRef.current;

// Inside createEngine() — plain functions, no hooks
function createEngine(setters) {
  const E = {
    products:  null,
    results:   {},
    isRunning: false,
    pollTimer: null,
  };

  E.schedulePoll = function(attempt) {
    // Always calls E.runLoop — the SAME function, not a stale copy
    E.pollTimer = setTimeout(async () => {
      if ((await probeCredits()).ok) E.runLoop(); // ✅ always live
      else E.schedulePoll(attempt + 1);
    }, 60000);
  };

  E.runLoop = async function() { ... };

  return E;
}
?
Rule of thumb: If you have async loops that call each other (processing → polling → processing), keep them in a plain object, not React hooks. Use React state only for rendering. Pass setter functions (setUiStatus, setUiResults) into the engine so it can update the UI without being part of React's render cycle.

Add console logging to verify

During development, add console.log statements at key points in the engine. Open browser DevTools → Console and watch for [SEO] probe result: {ok: true} followed by [SEO] runLoop started. This confirms auto-resume is firing correctly.

Debug logging
// In schedulePoll, after probe:
console.log("[SEO] probe result:", probe);

// At the start of runLoop:
console.log("[SEO] runLoop started, todo:", todo.length);
+
Summary

Full Enhancement Timeline

Phase 1 · Initial build

First working prototype

React artifact with CSV upload, Claude API batching, live preview, and download. Batch size 5, max_tokens 1000.

Bug discovered

JSON truncation — 0 products processed

max_tokens too low caused API responses to be cut off mid-JSON, breaking JSON.parse() on every batch.

Phase 2 · Fix

max_tokens 4,000 · batch size 2 · individual retry

All 1,557 products now processable. JSON parse errors fall back to per-product retry before skipping.

Phase 3 · Feature

localStorage persistence + pause/resume

Progress saved after every batch. Restore banner on page load. Resume skips already-processed products automatically.

Bug discovered

Extra rows added to CSV on download

Custom JS CSV parser split multiline quoted fields (HTML bullet lists) into multiple rows.

Phase 4 · Fix

PapaParse for read and write

Replaced custom parser with PapaParse. Used Papa.unparse() with quotes: true. Only 4 SEO columns updated.

Phase 5 · Feature

Auto-pause + auto-resume on credit limit

Detects credit errors, shows countdown, probes every 60s, resumes automatically when credits refresh.

Bug discovered

Auto-resume fires but nothing happens

useCallback stale closures meant schedulePoll was calling a frozen initial version of runLoop.

Phase 6 · Fix

Engine pattern — plain object in useRef

All processing logic moved to a plain createEngine() object. No hooks, no stale closures. Auto-resume now works reliably.

Reference

Build Checklist

Use this as a reference when building your own version. Each item represents a lesson learned from this build.

Ready to build your own?

Open Claude.ai, upload your product CSV, and start with the prompt below. The entire tool was built through conversation — no code editor required.

Open Claude.ai →
Starter prompt
"Analyse my product CSV and build a React artifact that uses the Claude API to generate optimised SEO page titles (50-60 chars), meta descriptions (150-160 chars), search keywords and meta keywords for each product. Process in batches of 2, save progress to localStorage, and output a BigCommerce-compatible CSV."

If you want to save time you can download the file we have developed and enhance it using Claude to meet your own requirements. Download Now