Blog API
Endpoints for managing AI-generated blog articles (blog). There are two families of blogs, both
stored in the same blog collection.
| Origin | Produced by | Entry point |
|---|---|---|
rewrite |
blog_rewriter (rewrite of a past blog) | POST /v1/old_blogs/{old_blog_id}/rewrite, POST /v1/blogs/generation |
generation |
blog_generation (newly written race recap / prediction) | POST /v1/blogs/generate |
origin is not stored; it is derived from the presence of old_blog_id.
Blog status
| Status | Description |
|---|---|
| generating | Generating; AI generation / build is in progress |
| draft | Draft (default) |
| published | Published |
generating cannot be supplied as input to POST /v1/blogs (it returns 400).
Blog schema
| Field | Type | Notes |
|---|---|---|
| id | string | _id |
| name | string | Title; also baked into the body HTML as a heading |
| race_id | string | null | _id of the linked race |
| race_type | enum | null | auto_racing / bicycle_racing / horse_racing |
| blog_type | string | null | Series type (e.g. jockey interview) |
| origin | enum | rewrite / generation, derived from old_blog_id |
| old_blog_id | string | null | _id of the source past blog |
| source_url | string | null | Original URL of the source blog |
| category_id | string | null | category._id |
| category | string | null | Category name resolved from category_id; null for deleted or invalid ids |
| body_html | string | HTML built by blog_builder |
| status | enum | generating / draft / published |
| created_at | number | UNIX timestamp (ms) |
| updated_at | number | UNIX timestamp (ms) |
POST /v1/blogs/generate
Overview
Writes a new blog from race data (no source article).
What gets written depends on kind.
| kind | Content | Input | Precondition |
|---|---|---|---|
recap (default) |
Race recap | Race result, race video, course image (horse racing only) | The race has a finishing order (not void / cancelled) |
prediction |
Race prediction | Entry list and AI prediction marks (race.prediction) |
Prediction data has been registered |
The blog is created in generating state and a job is dispatched to the blog_generation Lambda over
SQS (the blog-generation queue).
The category is linked automatically ("レース結果・回顧" for recaps, "レース予想・検証" for predictions). If no such category record exists, the blog is created without one.
Request and Response
Headers
Request
Content-Type: application/json
URI
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
| race_id | string | ◯ | Race _id |
| kind | enum | recap / prediction. Default: recap |
|
| name | string | Title. If omitted, it is composed from the race name ("…のレース回顧" / "…のレース予想") |
Response
202 Accepted
Error handling
| Description | Status code | Status |
|---|---|---|
| Invalid request body | 422 | Unprocessable Entity |
| Race not found | 404 | Not Found |
kind=prediction but no prediction data registered |
422 | Unprocessable Entity |
kind=recap but the race has no finishing order |
422 | Unprocessable Entity |
| Internal server error | 500 | Internal Server Error |
Processing flow
flowchart TD
Start([POST /v1/blogs/generate]) --> Race[Fetch race by race_id]
Race -->|not found| E404[404 Not Found]
Race -->|kind=prediction| PredCheck[Check prediction exists]
Race -->|kind=recap| VoidCheck[Check finishing order exists]
PredCheck -->|missing| E422[422 Unprocessable Entity]
VoidCheck -->|none| E422
PredCheck -->|OK| Category[Resolve category]
VoidCheck -->|OK| Category
Category --> Save[Save blog as generating]
Save --> SendSQS[Send to blog-generation queue]
SendSQS --> Success[202 Accepted]
Route ordering
/blogs/generate is declared before /blogs/{blog_id}; otherwise generate would be interpreted
as a blog_id.
POST /v1/blogs/generation
Overview
Starts a rewrite of a past blog (old_blog). Unlike
POST /v1/old_blogs/{old_blog_id}/rewrite, the blog name and category can be specified here.
The blog is created in generating state and a job is dispatched to the blog_rewriter Lambda over
SQS (the blog-rewrite queue). Race linkage, series type, source URL and the embedding vector are
carried over from old_blog.
URI
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
| old_blog_id | string | ◯ | old_blog._id |
| category_id | string | category._id |
|
| name | string | Title. Defaults to old_blog.name |
Response
202 Accepted
Error handling
| Description | Status code | Status |
|---|---|---|
old_blog_id not found |
400 | Bad Request |
| Internal server error | 500 | Internal Server Error |
POST /v1/blogs
Overview
Creates a blog manually.
Request body
{
"name": "string",
"body_html": "string",
"race_id": "string",
"race_type": "horse_racing",
"old_blog_id": "string",
"category_id": "string",
"blog_type": "string",
"status": "draft"
}
| Field | Type | Required | Notes |
|---|---|---|---|
| name | string | ◯ | |
| body_html | string | ◯ | HTML |
| race_id | string | Race _id |
|
| race_type | enum | auto_racing / bicycle_racing / horse_racing |
|
| old_blog_id | string | old_blog._id |
|
| category_id | string | category._id |
|
| blog_type | string | Series type | |
| status | enum | draft / published. Defaults to draft; generating is rejected |
Response
201 Created — returns the blog schema.
Error handling
| Description | Status code | Status |
|---|---|---|
| Invalid request body | 422 | Unprocessable Entity |
status set to generating |
400 | Bad Request |
| Internal server error | 500 | Internal Server Error |
GET /v1/blogs
Overview
Lists blogs.
URI
Query parameters
| Parameter | Type | Required | Notes |
|---|---|---|---|
| id | string | Exact blog-id match. Values that are not valid ObjectIds return no results | |
| name | string | Partial, case-insensitive title match | |
| race_id | string | Filter by race | |
| race_type | enum | Filter by auto_racing / bicycle_racing / horse_racing |
|
| blog_type | string | Filter by series type | |
| category_id | string | Filter by category | |
| origin | enum | Filter by rewrite / generation (translated to presence of old_blog_id) |
|
| status | enum | Filter by generating / draft / published |
|
| page | number | Page number. Default: 1 |
|
| size | number | Items per page. Default: 10 |
|
| order | enum | asc / desc (by updated_at). Default: desc |
Response
200 OK
{
"list": [
{
"id": "string",
"name": "string",
"race_id": "string",
"race_type": "horse_racing",
"blog_type": null,
"origin": "rewrite",
"old_blog_id": "string",
"source_url": "string",
"category_id": "string",
"category": "レース結果・回顧",
"body_html": "string",
"status": "draft",
"created_at": 1234567890000,
"updated_at": 1234567890000
}
],
"current_page": 1,
"total_count": 10
}
Category names are resolved in bulk for the list; looking them up per id would be an N+1.
GET /v1/blogs/{blog_id}
Overview
Fetches a single blog.
Response
200 OK — returns the blog schema.
Error handling
| Description | Status code | Status |
|---|---|---|
| Blog not found | 404 | Not Found |
| Internal server error | 500 | Internal Server Error |
PUT /v1/blogs/{blog_id}
Overview
Updates a blog.
Updating the title also rewrites the body heading
The title (name) is baked into the body HTML as an h2 heading. Updating only name would
leave a stale heading, so the server rewrites it as well. When body_html is updated in the same
request, the supplied body_html wins.
Request body
{
"name": "string",
"body_html": "string",
"old_blog_id": "string",
"category_id": "string",
"blog_type": "string",
"status": "published"
}
| Field | Type | Required | Notes |
|---|---|---|---|
| name | string | Title | |
| body_html | string | HTML | |
| old_blog_id | string | old_blog._id |
|
| category_id | string | category._id |
|
| blog_type | string | Series type | |
| status | enum | generating / draft / published |
Fields left out (null) are not updated.
Response
200 OK — returns the updated blog schema.
Error handling
| Description | Status code | Status |
|---|---|---|
| Invalid request body | 422 | Unprocessable Entity |
category_id given but the category does not exist |
400 | Bad Request |
| Blog not found | 404 | Not Found |
| Internal server error | 500 | Internal Server Error |
DELETE /v1/blogs/{blog_id}
Overview
Soft-deletes a blog.
Response
204 No Content
Error handling
| Description | Status code | Status |
|---|---|---|
| Blog not found | 404 | Not Found |
| Internal server error | 500 | Internal Server Error |
POST /v1/blogs/{blog_id}/thumbnail_generate
Overview
Starts thumbnail generation for a blog.
A job is dispatched to the thumbnail_generation Lambda over SQS (the thumbnail-generation queue).
The Lambda extracts the frame at an AI-selected moment of the race video, stores it in S3, and embeds
it into the blog body just below the "10-second summary" as <img class="nb-thumbnail">.
Blogs that are not tied to a race cannot have a thumbnail generated (there is no race video).
URI
Response
202 Accepted (no body)
Error handling
| Description | Status code | Status |
|---|---|---|
| Blog not found | 404 | Not Found |
Blog is still generating (status=generating) |
400 | Bad Request |
| Blog is not tied to a race | 400 | Bad Request |
| Internal server error | 500 | Internal Server Error |
POST /v1/blogs/{blog_id}/related_generate
Overview
Generates the related-information section (links to similar blogs) and inserts it into the body HTML.
The blog's embedding is compared by cosine similarity against other blogs that have embeddings, and the two closest are inserted as a "関連情報" section at the bottom of the article.
Candidates considered "the same article" are excluded.
| Exclusion | Reason |
|---|---|
Blogs rewritten from the same old_blog |
Their vectors are identical |
| Blogs with an identical title | Duplicate registration of the same article |
| Blogs with similarity ≥ 0.99 | Effectively identical content |
Blogs still generating (status=generating) are never candidates. A race card inserted at build time
stays at the top of the section after regeneration.
This endpoint is synchronous: it updates the body and returns it.
URI
Response
200 OK — returns the updated blog schema.
Error handling
| Description | Status code | Status |
|---|---|---|
| Blog not found | 404 | Not Found |
Blog is still generating (status=generating) |
400 | Bad Request |
| Blog has no embedding vector | 400 | Bad Request |
| No related-blog candidates | 400 | Bad Request |
| Internal server error | 500 | Internal Server Error |
Processing flow
flowchart TD
Start([POST /v1/blogs/blog_id/related_generate]) --> Fetch[Fetch the blog]
Fetch -->|not found| E404[404 Not Found]
Fetch -->|generating| E400[400 Bad Request]
Fetch -->|no embedding| E400
Fetch -->|OK| Candidates[Fetch other blogs with embeddings]
Candidates --> Select[Drop same-article candidates, take top 2 by cosine similarity]
Select -->|none| E400
Select -->|OK| Insert[Insert the related section into body_html]
Insert --> Success[200 OK]