Files
Michele fe6cd4d516 docs: complete project research
Files:
- STACK.md
- FEATURES.md
- ARCHITECTURE.md
- PITFALLS.md
- SUMMARY.md

Key findings:
- Stack: FastAPI 0.135.1 + React 19 + Vite 7 + Tailwind v4, single-container deploy
- Architecture: FastAPI serves React SPA via catch-all, file-based storage (Docker volume), LLMService with retry/backoff
- Critical pitfall: All 9 pitfalls map to Phase 1 — Italian prompts, Canva field constants, UTF-8 BOM, root_path config, per-item bulk isolation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 14:06:44 +01:00

26 KiB

Pitfalls Research

Domain: LLM-powered bulk content generation / CSV output for design tools Project: PostGenerator — Instagram carousel bulk generation for B2B Italian SME marketing Researched: 2026-03-07 Confidence: HIGH (most pitfalls verified against official docs or multiple confirmed sources)


Critical Pitfalls

Pitfall 1: LLM Output That "Looks Valid" But Isn't (Soft Failures)

What goes wrong: Claude returns HTTP 200 with a valid response, JSON parses successfully, but the content is wrong: truncated mid-sentence, repeated slide text, missing slides in a carousel, or Italian that reads as translated-from-English. These "soft failures" never trigger error handling logic because there is no exception — only bad output silently written to files.

Why it happens: Developers test the happy path, see valid JSON, and assume correctness. Validation is wired only for parse errors, not for semantic/structural correctness. In bulk mode, one bad generation gets buried in a batch of 20 and is only caught by the end user.

How to avoid:

  • Implement a two-level validation layer: (1) JSON schema validation via Pydantic on parse, (2) business rule validation on content — check slide count matches requested count, each slide has non-empty text fields, text lengths are within Canva field limits, no repeated content across slides.
  • Log structured validation failures separately from API errors so you can track quality drift.
  • For Italian content: detect obvious English leakage by checking for common English stop words ("the", "and", "of") in output fields that should be Italian.

Warning signs:

  • Output carousels where multiple slides have identical or near-identical text.
  • Generated titolo fields that are suspiciously short (< 5 chars) or over Canva limits.
  • CSV rows where some cells are empty when the schema required content.
  • User reports "Canva mappa un campo ma c'e' testo mancante".

Phase to address: Phase 1 (Core generation pipeline) — build validation alongside the first API call, not as a later addition.


Pitfall 2: Canva Bulk Create CSV — Column Names Must Exactly Match Template Placeholders

What goes wrong: Canva Bulk Create requires CSV column headers to exactly match the placeholder names defined in the Canva template design. A mismatch (case, space, accent, or encoding difference) causes the field to silently fail to map — the placeholder stays empty in the generated design. There is no error message; the design just looks incomplete.

Why it happens: Developers generate CSV column names programmatically from the content schema, but the Canva template was built manually with slightly different placeholder names. The user then has to manually re-map every field in the Canva UI, defeating the automation benefit.

How to avoid:

  • Define placeholder names as a project-level constant (e.g. CANVA_FIELDS = ["titolo", "sottotitolo", "testo_1", ...]) that is shared between the template documentation and the CSV generator code.
  • Include a "validate against template" step in the generation pipeline: before writing the CSV, verify every column name matches the expected constant list.
  • Document exact Canva placeholder names in the project (e.g., in a CANVA_TEMPLATE.md or in the prompt templates themselves).

Warning signs:

  • User says "Canva non collega automaticamente i campi, devo farlo a mano ogni volta".
  • CSV preview in the application shows correct data but Canva designs come out blank.
  • After a prompt template change, the column name changes and existing Canva templates break.

Phase to address: Phase 1 — define and lock the Canva field schema before writing any generation code.


Pitfall 3: CSV UTF-8 BOM Encoding Breaks Canva / Excel Import

What goes wrong: The CSV is correctly UTF-8 encoded, but Canva or the user's spreadsheet tool misinterprets it because a BOM (Byte Order Mark) is present or absent. Italian accented characters (à, è, é, ì, ò, ù) appear as à , è etc. in the design. Alternatively, the file is generated without BOM, and when the user opens it in Excel on Windows before uploading to Canva, Excel re-encodes it to Windows-1252, corrupting the accented characters before they reach Canva.

Why it happens: Python's csv module with encoding='utf-8' produces UTF-8 without BOM. Excel on Windows expects UTF-8 with BOM (utf-8-sig) to auto-detect encoding. Canva itself expects plain UTF-8. This creates a two-failure-mode trap: BOM for Excel users, no-BOM for Canva direct upload.

How to avoid:

  • Generate CSV with encoding='utf-8-sig' (UTF-8 with BOM). Canva ignores the BOM; Excel on Windows correctly reads it. This is the "safe default" for this use case.
  • Always include a charset declaration in the HTTP response header when serving the download: Content-Type: text/csv; charset=utf-8.
  • Test the download with actual Italian content containing àèéìòù before declaring the feature complete.
  • Add a note in the UI: "Apri il CSV in Google Sheets, non in Excel, per evitare problemi di encoding".

Warning signs:

  • Any accented character appearing as multiple garbled characters in Canva designs.
  • User reports "i caratteri speciali sono sbagliati".
  • CSV looks fine in a code editor but breaks when opened in Excel.

Phase to address: Phase 1 — encoding must be correct from the first CSV output, not retrofitted later.


Pitfall 4: FastAPI root_path Double-Path Bug Behind Nginx Subpath Proxy

What goes wrong: FastAPI deployed at lab.mlhub.it/postgenerator/ requires root_path configuration to generate correct OpenAPI/Swagger URLs. However, if root_path is set both in FastAPI() constructor AND in Uvicorn, the prefix is applied twice, producing paths like /postgenerator/postgenerator/openapi.json which returns 404. The API may work correctly while the docs are broken, masking the configuration error.

Why it happens: The FastAPI root_path mechanism is designed for "stripping path proxies" that remove the prefix before forwarding. If the nginx config forwards the full path (including /postgenerator/), FastAPI needs root_path to know its prefix but must NOT double-apply it. The interaction between FastAPI app-level root_path and Uvicorn-level root_path is a confirmed multi-year bug with many GitHub issues.

How to avoid:

  • Set root_path only via Uvicorn (--root-path /postgenerator) or as an environment variable, NOT in the FastAPI() constructor.
  • Configure nginx to strip the prefix before forwarding to the container: proxy_pass http://container:8000/; (note trailing slash — nginx strips the location prefix).
  • Test OpenAPI docs at lab.mlhub.it/postgenerator/docs explicitly during initial deploy, not just API calls.
  • Use X-Forwarded-Prefix header instead of root_path if nginx stripping is not feasible.

Warning signs:

  • Swagger UI loads but API calls from the docs return 404.
  • /openapi.json returns 404 while direct API endpoint calls work.
  • Browser network tab shows requests to ../postgenerator/postgenerator/....

Phase to address: Phase 1 (deployment scaffolding) — verify proxy configuration before any feature work.


Pitfall 5: Bulk Generation Without Per-Item State — All-or-Nothing Failure

What goes wrong: A user requests 20 carousels. The backend loops through 20 API calls sequentially. The 15th call hits a rate limit or network timeout. The entire batch fails, the partial results are lost, and the user must start over from scratch.

Why it happens: Simple sequential loop with a try/except at the top level. No intermediate state is persisted. This is acceptable for a single-item generation but catastrophic for bulk operations where each item has real API cost.

How to avoid:

  • Implement per-item status tracking: each carousel gets a status record (pending / processing / success / failed) stored in the file system or an in-memory dict with a job ID.
  • On failure, mark the item as failed and continue processing the rest. Allow retry of only failed items.
  • Return partial results: if 15/20 succeed, deliver those 15 in the CSV.
  • Persist the job state to disk (JSON file per job) so that server restart does not lose progress.

Warning signs:

  • Bulk requests timeout and the user sees no output.
  • Backend logs show one exception that kills the loop.
  • User has to re-enter all 20 topics to retry because no intermediate state exists.

Phase to address: Phase 1 for basic per-item error isolation; Phase 2+ for full job persistence and retry UI.


Pitfall 6: Rate Limit Errors Treated as Generic Errors — No Backoff

What goes wrong: Claude API returns HTTP 429 (rate limit exceeded). The backend catches Exception, logs "generation failed", and either retries immediately (hammering the API) or returns an error to the user. In Tier 1 (50 RPM, 8,000 OTPM for Sonnet), a bulk batch of 10-20 carousels can easily hit the OTPM ceiling.

Why it happens: Developers test with 1-2 items and never hit rate limits. Error handling is added generically. The retry-after header in the 429 response — which tells you exactly how long to wait — is ignored.

How to avoid:

  • Implement specific 429 handling that reads the retry-after response header and waits that exact duration before retrying.
  • Use exponential backoff with jitter for other transient errors (5xx), but honor retry-after precisely for 429.
  • For bulk jobs: add a configurable delay between consecutive API calls (e.g. 2-3 seconds) to stay within OTPM limits even in Tier 1.
  • Monitor anthropic-ratelimit-output-tokens-remaining response header to throttle proactively.
  • Consider the Batches API (50% cheaper, separate rate limits) for non-interactive bulk generation.

Warning signs:

  • Backend logs full of 429 errors during any batch larger than 5 items.
  • Generation speed suddenly drops to zero mid-batch.
  • Costs appear lower than expected (actually requests are failing, not succeeding).

Phase to address: Phase 1 — implement from the first API integration, not as a later optimization.


Pitfall 7: Prompt Templates With Hard-Coded Assumptions That Break Silently

What goes wrong: The prompt template instructs Claude to "generate 5 slides". Later, the user configures 7 slides per carousel. The template still says 5, but the schema expects 7. Claude generates 5 slides. The schema validator accepts it (minimum not enforced). CSV has 5 slide columns with data and 2 empty. Canva design has 2 blank slides.

Why it happens: Prompt template is written with specific numbers embedded as literals rather than as injected variables. When configuration changes, only the schema/code is updated, not the template. File-based prompt management makes this disconnect invisible — there is no compile-time check.

How to avoid:

  • Make all variable parameters injectable: Genera {{num_slides}} slide, not "Genera 5 slide".
  • Implement a template validation step at startup: parse all templates, identify all {{variable}} placeholders, and verify every placeholder has a corresponding runtime value.
  • Use a template rendering test in CI: render each template with test values and verify output matches expected format.
  • Keep a TEMPLATE_VARIABLES.md that documents every variable each template expects.

Warning signs:

  • Generated carousels have inconsistent slide counts.
  • Template file was last modified weeks ago but configuration was changed last week.
  • New team member added a config option but forgot to update the prompt template.

Phase to address: Phase 1 (prompt system foundation) — variable injection must be architectural, not an afterthought.


Pitfall 8: Italian Language — Prompts Written in English Produce "Translated" Italian

What goes wrong: The system prompt is written in English and asks Claude to "generate content in Italian". Claude generates Italian that is grammatically correct but sounds translated: unnatural phrasing, English idioms translated literally, formal register too stiff for SME B2B social media, or UK English business vocabulary instead of Italian business vocabulary.

Why it happens: Claude is primarily English-trained. When given English instructions to produce Italian output, it "thinks in English and translates". The result passes spell-check but fails the native speaker smell test. B2B content for Italian entrepreneurs has specific vocabulary and register expectations (professional but not academic, action-oriented, concrete benefits).

How to avoid:

  • Write the system prompt IN Italian (not "write in Italian" from an English prompt). Italian-language instructions produce more natural Italian output.
  • Provide Italian examples in the few-shot section: show Claude an example carousel with the exact tone, vocabulary, and structure you want.
  • Define explicit tone guidelines in Italian: e.g. "Usa il tu, sii diretto, parla di benefici concreti, evita il jargon tecnico".
  • Include a list of Italian B2B vocabulary to use/avoid.
  • Note: Italian prompts cost ~2x more tokens than English equivalents — factor this into cost estimates.

Warning signs:

  • Generated Italian text uses "fare business" when native would say "fare affari".
  • Carousel titles that sound like SEO headlines rather than Instagram hooks.
  • Formality that oscillates randomly (tu/lei mixed, formal/informal register).
  • User feedback: "sembra scritto con Google Translate".

Phase to address: Phase 1 — prompt language and tone are foundational decisions, hard to fix post-deployment without regenerating all content.


Pitfall 9: React Frontend API URL Hardcoded to Absolute Path — Breaks Behind Subpath Proxy

What goes wrong: React frontend makes API calls to /api/generate. Behind nginx at lab.mlhub.it/postgenerator/, the actual path is lab.mlhub.it/postgenerator/api/generate. The frontend sends requests to lab.mlhub.it/api/generate which is a different nginx location (or 404). Works in local development, breaks immediately in production.

Why it happens: Developers use Create React App / Vite proxy in development where /api works fine. In production, the subpath prefix is not accounted for. REACT_APP_API_URL environment variable is set to /api (absolute) instead of a relative or base-path-aware URL.

How to avoid:

  • Use the Vite/CRA VITE_API_BASE_URL env var set to an empty string for development (proxy handles it) and /postgenerator/api for production builds.
  • Alternatively: serve the React app and FastAPI from the same container with nginx internal routing — browser sees single origin, no CORS, no subpath math.
  • Test the production build (not dev server) against the nginx proxy during Phase 1 deployment, before any feature work.
  • In nginx config, ensure /postgenerator/api/ proxies to FastAPI and /postgenerator/ serves the React static files.

Warning signs:

  • API calls return 404 or hit the wrong service in production but work in npm run dev.
  • Browser network tab shows requests to /api/generate without the /postgenerator prefix.
  • CORS errors in production (requests going to wrong origin because URL resolution failed).

Phase to address: Phase 1 deployment scaffolding — must be tested before any other development.


Technical Debt Patterns

Shortcut Immediate Benefit Long-term Cost When Acceptable
Store all files in flat directory without job subdirectories Simple to implement Hundreds of files mixed together, no cleanup possible Never — use job-scoped directories from day 1
Hardcode slide count (5) in prompt template Fast to write Config changes break output silently Never — always inject as variable
Single try/except around entire generation loop Simple error handling One failure kills entire batch Never for bulk — per-item isolation is required
Write CSV as UTF-8 without BOM Python default Italian accents corrupt in Excel Never — use utf-8-sig always
Set root_path in both FastAPI and Uvicorn Seems comprehensive Double-path 404 bug in docs and some calls Never — set in one place only
Generate all carousels before validating any Defers complexity User waits for all, then gets bulk failure Acceptable in MVP if per-item failure isolation exists
English-language system prompt with "write in Italian" Easier to write Translated-sounding Italian output Never for public-facing product

Integration Gotchas

Integration Common Mistake Correct Approach
Claude API structured outputs Using schema with minimum/maximum constraints and expecting them to be enforced Constraints are stripped from schema sent to Claude; use Pydantic validation post-response to enforce ranges
Claude API rate limits Treating 429 as generic error, retrying immediately Read retry-after header; honor exactly; add inter-request delay for bulk jobs
Canva Bulk Create Column names generated from code schema diverge from template placeholders Lock column names as constants shared between code and Canva template documentation
Canva Bulk Create Uploading CSV with row count > 300 Canva Bulk Create supports max 300 rows per upload batch; split large batches
Claude API OTPM Generating 20 carousels at full speed in Tier 1 (8,000 OTPM) Add configurable delay between calls; consider Batches API for non-interactive generation
FastAPI + nginx subpath Setting root_path in FastAPI constructor when nginx does NOT strip prefix Set root_path only in Uvicorn; configure nginx to strip prefix OR forward full path but not both
React + FastAPI in same Docker container CORS configuration in FastAPI needed even for same-origin Use nginx internal routing so browser sees single origin; CORS becomes irrelevant

Performance Traps

Trap Symptoms Prevention When It Breaks
Sequential API calls for bulk 20 carousels take 20x single time; UI shows "loading" with no feedback Add progress tracking per item; consider async generation with status polling Any bulk request > 3 items
No prompt caching for repeated system prompt Each call sends full system prompt, consuming ITPM quota Use cache_control: ephemeral on system prompt; same prompt cached for ~5 min At Tier 1 OTPM limits with 5+ items in batch
CSV generated in memory for large batches Memory spike, potential OOM in 256MB container Stream CSV rows to disk as each carousel is generated Batches > 50 carousels
File storage without cleanup Disk fills up on VPS over time Implement TTL-based cleanup for generated files (e.g., delete after 24h) After ~1000 generation jobs depending on file size

Security Mistakes

Mistake Risk Prevention
Exposing Claude API key in frontend environment variables Key leaked via browser DevTools, used for unauthorized API calls Keep API key server-side only; frontend never sees it
No validation of user-provided topic/industry input before injecting into prompt Prompt injection: user crafts input that overrides system prompt instructions Sanitize and length-limit user inputs; wrap user content in explicit delimiters in prompt
Storing generated CSV files accessible at predictable URLs Users can download others' generated content Use UUID-based job IDs for file paths; validate job ownership before serving
No API rate limiting on the FastAPI endpoint Unlimited calls = unlimited Claude API cost Implement per-IP or per-session rate limiting on the generation endpoint

UX Pitfalls

Pitfall User Impact Better Approach
No progress feedback during bulk generation User sees spinner for 60+ seconds, assumes crash, refreshes, loses job Show per-carousel progress (item 3/10 completed) with estimated time remaining
"Generation failed" error with no actionable info User does not know if it was their input, the API, or a bug Distinguish: "API limit reached, retry in X seconds" vs "Input too long" vs "Unexpected error (ID: xxx)"
CSV download triggers browser "open with" dialog User opens CSV in Excel, encoding corrupts Italian text, blames the tool Set Content-Disposition: attachment; filename="carousels.csv" and add note about Google Sheets
Generated content not previewable before CSV download User discovers quality issues only after importing to Canva Show a text preview of at least the first carousel before offering CSV download
No way to regenerate a single carousel User must redo entire batch to fix one bad result Allow per-item regeneration from the results view

"Looks Done But Isn't" Checklist

  • CSV encoding: Contains actual Italian accented characters (àèéìòù) — verify download in both browser and Excel, not just code editor
  • Canva field mapping: Test actual Canva import with the generated CSV, not just "the columns look right"
  • Rate limit handling: Test with a batch of 10+ items to trigger actual rate limits in Tier 1
  • Subpath routing: Test production build (Docker container) at nginx subpath, not local npm run dev
  • FastAPI docs: Verify Swagger UI at /postgenerator/docs works AND API calls from within Swagger work
  • Italian quality: Have a native Italian speaker review generated content, not just verify it is grammatically Italian
  • Partial failure: Kill the process mid-batch and verify partial results are not lost
  • Large batch: Test with 20 carousels (near Tier 1 limits) for both correctness and timing
  • Prompt variable injection: Change slide count in config, verify prompt template reflects the change, verify output slide count matches

Recovery Strategies

Pitfall Recovery Cost Recovery Steps
CSV encoding corruption LOW Change Python writer to utf-8-sig, regenerate affected CSVs
Canva field name mismatch MEDIUM Redefine constant, update all templates referencing old names, regenerate
FastAPI double root_path LOW Remove root_path from FastAPI constructor, set only in Uvicorn, redeploy
Italian quality issues in prompt HIGH Rewrite system prompt in Italian, add few-shot examples, re-evaluate all previously generated content
Bulk pipeline data loss (no per-item state) HIGH Requires architectural change to generation loop; cannot fix without refactor
Prompt template variable mismatch MEDIUM Add validation step, fix template, test all templates in sequence

Pitfall-to-Phase Mapping

Pitfall Prevention Phase Verification
Soft failures in LLM output (Pitfall 1) Phase 1: Generation pipeline Validation rejects malformed output in unit tests
Canva column name mismatch (Pitfall 2) Phase 1: Define Canva field constants Manual Canva import test with generated CSV
CSV UTF-8 BOM encoding (Pitfall 3) Phase 1: First CSV output Open downloaded CSV in Excel on Windows — no garbled chars
FastAPI root_path double bug (Pitfall 4) Phase 1: Deployment scaffolding Swagger UI works at /postgenerator/docs in Docker
Bulk batch all-or-nothing failure (Pitfall 5) Phase 1: Per-item error isolation Force mid-batch failure, verify partial results saved
Rate limit no-backoff (Pitfall 6) Phase 1: API client layer Batch of 10 items completes without 429 crashing the job
Prompt template hardcoded values (Pitfall 7) Phase 1: Prompt template system Change slide count config, verify template output changes
Italian quality / translated-sounding (Pitfall 8) Phase 1: Prompt engineering Native Italian speaker review before any user testing
React API URL subpath bug (Pitfall 9) Phase 1: Deployment scaffolding Production build test at nginx subpath URL

Sources


Pitfalls research for: PostGenerator — LLM-powered Instagram carousel bulk generation (B2B Italian SME) Researched: 2026-03-07