Skip to content

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

POST /v1/blogs/generate

Request body

{
  "race_id": "string",
  "kind": "recap",
  "name": "string"
}
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

{
  "id": "string"
}

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

POST /v1/blogs/generation

Request body

{
  "old_blog_id": "string",
  "category_id": "string",
  "name": "string"
}
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

GET /v1/blogs

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

POST /v1/blogs/{blog_id}/thumbnail_generate

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

POST /v1/blogs/{blog_id}/related_generate

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]