Content & Integrations

Docs API

Content & Integrations

Blog metadata, mood comments by post id, GitHub contributions, the MusicKit developer token, and the posts endpoint that is not switched on yet.

Five endpoints that pull something from outside the site and hand it back as JSON. They have almost nothing in common except that each one is a cache in front of a third party, and each one fails differently when that third party is having a bad day.

Writing

GET /api/writing

The latest posts from the Ghost blog, as the home page renders them. No auth. Rate limit: 60 requests / 60s. Cached hard — public, s-maxage=3600, stale-while-revalidate=86400 — because the blog does not change on the minute and this endpoint sits in front of a Ghost Content API call.

JSON
{
  "ghostUrl": "https://blog.buxx.me",
  "posts": [
    {
      "id": "6512c0f3a9b1e40001d2c4aa",
      "title": "無人之境",
      "url": "https://blog.buxx.me/wu-ren-zhi-jing/",
      "published_at": "2026-08-14T09:00:00.000Z",
      "tags": [{ "id": "…", "name": "Essays", "slug": "essays", "visibility": "public" }]
    }
  ]
}

Always exactly the five newest posts; there is no limit parameter. tags can include entries with visibility: "internal" — Ghost’s convention for tags starting with #, which are used for routing and are not meant to be rendered as topic labels. Filter on visibility === "public" before displaying them.

There is no error branch: if Ghost is unreachable the handler returns 200 with an empty posts array rather than a 5xx. As with /api/footer, an empty result and a broken upstream look identical from the outside.

Comments by post id

GET /api/comments?postId=<id>

A legacy alias for the mood comment thread of a single post. It reads the same data as /api/mood/:id/comments through the live Telegram mirror, not the D1 archive — see Mood API for the response shape and the freshness trade-off.

postId is required and trimmed; omitting it (or sending only whitespace) returns 400 {"error":"Missing postId parameter"}. Note the flat error string here, while the mood comment payload it wraps uses the nested {"error":{"code","message"}} form — one route, both error shapes, depending on how far the request got.

New integrations should call the mood route directly. This one exists so old clients keep working.

GitHub contributions

GET /api/github/contributions?username=bunizao&days=365

The contribution grid on the home page. No auth. Rate limit: 60 requests / 60s. Cache-Control: no-store, max-age=0 on every response.

Parameter Type Default Notes
username string bunizao Must be bunizao. Any other login is rejected.
days integer 365 1365. Non-numeric or out of range is rejected, not clamped.
JSON
{
  "total": { "lastYear": 1284 },
  "contributions": [{ "date": "2026-08-23", "count": 4, "level": 2 }]
}

contributions is the trailing days window, oldest first, one entry per day. level is GitHub’s own 0–4 intensity bucket. total.lastYear is always the full-year total and does not shrink when you narrow days — a 30-day window still reports the annual count, so do not use it as the sum of the array you were given.

Errors: 400 {"error":"Unsupported GitHub username"} for any login other than bunizao — this is an allowlist, not an open proxy, so it will not fetch arbitrary users’ grids. 400 {"error":"Unsupported contribution window"} for a bad days. 429 {"error":"Too Many Requests"}. 503 {"error":"GitHub contributions unavailable"} when GitHub’s API cannot be reached — this one is retryable, and unlike /api/writing and /api/footer it does surface the failure. Any method other than GET gets a plain-text 405 Method Not Allowed.

Despite the no-store on the response, results are cached inside the Worker for 10 minutes per (username, days) pair, so hammering this endpoint does not hammer GitHub.

MusicKit developer token

GET /api/musickit/token

Mints a short-lived Apple MusicKit developer token so the browser can talk to Apple Music directly. Rate limit: 30 requests / 60s. Cache-Control: private, max-age=300private, because the token is credential material and must not land in a shared cache.

Errors: 503 {"error":"MusicKit is not configured"} when the signing key is absent from the environment, which is the normal state in local dev; 500 {"error":"MusicKit token unavailable"} if signing fails; 429 {"error":"Too Many Requests"}. Non-GET methods get a plain-text 405 Method Not Allowed.

/v2/musickit/token is a legacy alias of the same handler.

Posts (not enabled)

GET /api/v2/posts
GET /api/v2/posts/:slug

A placeholder. The route exists, is wired up, and returns nothing useful — it is reserved so the path is not claimed by anything else before the real implementation lands.

With the ENABLE_POSTS_API flag off (the current state everywhere), both return 404:

JSON
{ "error": { "code": "not_found", "message": "Posts API is not enabled." }, "endpoint": "/v2/posts" }

With the flag on, they return 501 with code posts_coming_soon instead, because the data layer behind them is still a stub. Either way there is no success path today. Cache-Control: no-store, max-age=0.

Use /api/writing for blog metadata, or the RSS feeds for full post content.