Skip to content

article_builder Lambda Overview

Triggered by messages on the SQS article-builder queue, this Lambda renders the "10-second race result summary" produced by article_generation into HTML with a React component and stores it as body_html on the article collection in DocumentDB, moving status from generating to draft.

It never moves an article to published โ€” that is an operator action.

Splitting article production into generation (article_generation) and HTML build/persistence (article_builder) keeps AI generation loosely coupled from rendering and database writes.


What body_html contains

body_html is the summary box alone โ€” a self-contained HTML fragment with its styles inlined. The race result page itself (results table, payouts, corner passing order, โ€ฆ) is rendered by jester-user-web, which embeds this summary inside it.

The colour theme follows racing_type.

racing_type Theme Colour
horse_racing keiba Green
bicycle_racing keirin Blue
auto_racing autorace Red

Tech stack

Because it uses React components directly, this is one of the two TypeScript (Node.js) Lambdas in the backend (the other is blog_builder); the rest are Python 3.12.

Item Details
Runtime Node.js 22
Language TypeScript
Rendering renderToStaticMarkup() from react-dom/server
Validation zod (articleBuilderPayloadSchema)
DB client mongodb (the official driver; Beanie is not used)
Bundling esbuild; handler.ts is bundled into dist/index.js at deploy time

Rendering notes

  • Nothing is built at invocation time. Bundling happens at deploy; the runtime only calls renderToStaticMarkup()
  • The output is pure HTML with no JavaScript (no hydration), so components must be display-only
  • CSS is inlined at bundle time and embedded in the HTML as a <style> element

Trigger

Sender When
article_generation Lambda When article generation completes

SQS event payload (input)

{
  "article_id": "string",
  "race_id": "string",
  "racing_type": "horse_racing",
  "content": {
    "headline": "string",
    "summary": ["string", "string", "string"]
  },
  "accuracy_test_data": {}
}
Field Type Required Notes
article_id string โ—ฏ _id of the article row to update (in generating state)
race_id string โ—ฏ Race _id
racing_type enum โ—ฏ auto_racing / bicycle_racing / horse_racing; selects the colour theme
content object โ—ฏ The generated 10-second summary (headline / summary)
accuracy_test_data object Accuracy-verification data; optional for compatibility with older messages

Processing flow

  1. Receive the SQS event and validate it with articleBuilderPayloadSchema
  2. Fetch the article by article_id. If it does not exist, exit without raising (guards against redelivery for a discarded article)
  3. Convert content into a RaceSummary and render it with renderRaceSummaryHtml()
  4. Store body_html, status=draft, updated_at (and accuracy_test_data when present)
flowchart TD
    ArticleGen([article_generation Lambda]) -->|SQS| Start
    Start[Receive SQS trigger] --> Parse[Validate payload with zod]
    Parse -->|invalid| Error[Throw]
    Parse -->|valid| Fetch[Look up article by article_id]
    Fetch -->|not found| Skip[Log and finish normally]
    Fetch -->|found| Render[Render RaceSummary to static markup]
    Render -->|failure| Error
    Render -->|success| Save[Store body_html and set status=draft]
    Save -->|failure| Error
    Save -->|success| Success[Finish]
    Error -->|final attempt| Discard[Hard-delete the generating article and finish]

What is written to DocumentDB

Field Update
body_html The article HTML built from the component
status generating โ†’ draft
updated_at Current time (UNIX ms)
accuracy_test_data Updated only when present in the payload

The only output is body_html in DocumentDB; nothing is written to S3.


Error handling

Situation Behaviour
Failure on a non-final attempt Re-throw and let SQS redelivery recover
Failure on the final attempt Hard-delete the status=generating article and finish without re-throwing

Only status=generating rows are deleted, so generated articles (draft / published) are never removed by mistake. The database convention is soft delete, but a row left generating with an empty body is an incomplete artifact that will never be published and gets in the way of regeneration.

Self-healing DocumentDB connections

If the database goes down briefly, the topology closes and a warm container keeps holding that broken client, failing from then on. When a connection-class error is detected, the client is discarded and re-established once, and the build is retried in the same invocation โ€” recovering without waiting for SQS redelivery (up to 90 minutes).


AWS services and libraries

Service / library Use
Amazon SQS Trigger (generation results from article_generation)
Amazon DocumentDB Updating article (body_html, status)
React / react-dom Rendering the RaceSummary component to a string
zod Validating the SQS payload
esbuild Bundling at deploy time