<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: GENISOFT WEB</title>
    <description>The latest articles on DEV Community by GENISOFT WEB (@genisoft_web_82fe21085cd1).</description>
    <link>https://dev.to/genisoft_web_82fe21085cd1</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4003067%2Fe90cd702-ff54-4a32-a5ba-355e8ecb17d4.png</url>
      <title>DEV Community: GENISOFT WEB</title>
      <link>https://dev.to/genisoft_web_82fe21085cd1</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/genisoft_web_82fe21085cd1"/>
    <language>en</language>
    <item>
      <title>How I Built an AI SEO Automation SaaS for PrestaShop — Architecture &amp; Lessons Learned</title>
      <dc:creator>GENISOFT WEB</dc:creator>
      <pubDate>Thu, 25 Jun 2026 23:15:38 +0000</pubDate>
      <link>https://dev.to/genisoft_web_82fe21085cd1/how-i-built-an-ai-seo-automation-saas-for-prestashop-architecture-lessons-learned-45ho</link>
      <guid>https://dev.to/genisoft_web_82fe21085cd1/how-i-built-an-ai-seo-automation-saas-for-prestashop-architecture-lessons-learned-45ho</guid>
      <description>&lt;p&gt;I run a small SaaS called Fexa AI — it automates SEO content generation and multilingual translation for PrestaShop stores. This is what I learned building it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fizamgjd3tasfy972vkji.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fizamgjd3tasfy972vkji.png" alt=" " width="799" height="304"&gt;&lt;/a&gt;&lt;br&gt;
The problem&lt;br&gt;
PrestaShop merchants have catalogs of hundreds (sometimes thousands) of products. Every product needs a unique title tag, meta description, ALT text, and structured data. Multiply by 4 languages = tens of thousands of manual tasks. Most merchants just skip it. Their SEO suffers. I wanted to automate this.&lt;/p&gt;

&lt;p&gt;Stack&lt;br&gt;
Frontend: Next.js 14 App Router + TypeScript&lt;br&gt;
Backend: REST APIs + Stripe webhooks, Prisma ORM on PostgreSQL (Neon)&lt;br&gt;
AI: Vercel AI SDK → DeepSeek with Google Gemini as auto-fallback&lt;br&gt;
Queue: Google Cloud Tasks for async bulk jobs&lt;br&gt;
Infra: Google Cloud Run (europe-west1)&lt;br&gt;
Store integration: PrestaShop WebService API via a free open-source module&lt;br&gt;
The hardest problem: Cloud Run timeouts&lt;br&gt;
Cloud Run has a 60-second request timeout. Translating a 3,000-product catalog in one HTTP request is a guaranteed timeout.&lt;/p&gt;

&lt;p&gt;My fix: chunking via Cloud Tasks.&lt;/p&gt;

&lt;p&gt;Instead of processing everything in one request, the API endpoint:&lt;/p&gt;

&lt;p&gt;Splits the job into chunks of ~50 products&lt;br&gt;
Enqueues one Cloud Task per chunk&lt;br&gt;
Each task hits a separate endpoint, processes its chunk, and exits&lt;br&gt;
Key detail: idempotency. Cloud Tasks can retry on failure. Without an idempotency key per chunk, a retry would re-bill the user's credits. I store a jobChunkId in the DB — if a task arrives twice, the second one returns 200 immediately.&lt;/p&gt;

&lt;p&gt;Credit atomicity&lt;br&gt;
Each AI generation costs credits. With parallel tasks running on the same account, you can race-condition your way into negative credits.&lt;/p&gt;

&lt;p&gt;Solution: PostgreSQL atomic decrement with a floor check — if the user doesn't have enough credits, the row update returns empty and the job is rejected before any AI call is made.&lt;/p&gt;

&lt;p&gt;AI consistency across 4 languages&lt;br&gt;
The naive approach (generate in French, then translate) produces poor results — Spanish SEO copy translated from French reads awkwardly.&lt;/p&gt;

&lt;p&gt;Better: send the original product data directly to the model with a locale-specific system prompt. Each language gets its own generation, not a translation of a translation.&lt;/p&gt;

&lt;p&gt;Stripe webhook reliability&lt;br&gt;
Stripe can replay webhooks. Without deduplication, a replayed checkout.session.completed would grant credits twice.&lt;/p&gt;

&lt;p&gt;Fix: store each stripeEvent.id in a unique DB table before processing. If it's already there, return 200 immediately.&lt;/p&gt;

&lt;p&gt;What I'd do differently&lt;br&gt;
Start with chunking — I built the single-request version first and had to refactor everything under production pressure.&lt;br&gt;
Idempotency from day one — retrofitting it is painful.&lt;br&gt;
Separate the AI layer — mixing AI orchestration with business logic in the same route handlers makes testing hard.&lt;br&gt;
Result&lt;br&gt;
One merchant translated and re-optimized 3,500 product pages in under 4 hours. That's the kind of result that makes the architecture complexity worth it.&lt;/p&gt;

&lt;p&gt;If you're building something similar (AI + async queues + Stripe), happy to answer questions in the comments.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="//fexaai.com"&gt;fexaai.com&lt;/a&gt; — free account, no credit card required.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>saas</category>
      <category>ai</category>
      <category>seo</category>
    </item>
  </channel>
</rss>
