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
- Receive the SQS event and validate it with
articleBuilderPayloadSchema - Fetch the
articlebyarticle_id. If it does not exist, exit without raising (guards against redelivery for a discarded article) - Convert
contentinto aRaceSummaryand render it withrenderRaceSummaryHtml() - Store
body_html,status=draft,updated_at(andaccuracy_test_datawhen 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 |