{
  "openapi": "3.1.0",
  "info": {
    "title": "Tirup Mehta Blogs API",
    "version": "1.0.0",
    "summary": "Public read-only API for blogs.tirup.in",
    "description": "Free, unauthenticated, read-only access to the published articles of blogs.tirup.in, the canonical engineering blog of Tirup Mehta. Every operation here is public. The write endpoints under /api/posts require the author's session cookie and are intentionally excluded. Article pages also support Markdown content negotiation: send `Accept: text/markdown` to receive the article as Markdown instead of HTML.",
    "contact": {
      "name": "Tirup Mehta",
      "url": "https://tirup.in/contact"
    }
  },
  "servers": [
    {
      "url": "https://blogs.tirup.in",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Feeds",
      "description": "Syndication and discovery documents."
    },
    {
      "name": "Articles",
      "description": "Published article content as JSON, HTML, or Markdown."
    }
  ],
  "paths": {
    "/feed.xml": {
      "get": {
        "operationId": "getRssFeed",
        "tags": ["Feeds"],
        "summary": "RSS 2.0 feed of published articles",
        "description": "Up to 50 most recent published articles, newest first. Each item carries title, canonical link, GUID, publication date, and the article TL;DR as the description. No authentication required.",
        "responses": {
          "200": {
            "description": "RSS 2.0 XML document.",
            "content": {
              "application/rss+xml": {
                "schema": {
                  "type": "string",
                  "description": "RSS 2.0 XML document with one <item> per published article."
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/FeedUnavailable"
          }
        }
      }
    },
    "/rss.xml": {
      "get": {
        "operationId": "getRssFeedAlias",
        "tags": ["Feeds"],
        "summary": "RSS 2.0 feed alias",
        "description": "Identical document to /feed.xml, kept for readers that expect the conventional alias.",
        "responses": {
          "200": {
            "description": "RSS 2.0 XML document.",
            "content": {
              "application/rss+xml": {
                "schema": {
                  "type": "string",
                  "description": "RSS 2.0 XML document with one <item> per published article."
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/FeedUnavailable"
          }
        }
      }
    },
    "/sitemap.xml": {
      "get": {
        "operationId": "getSitemap",
        "tags": ["Feeds"],
        "summary": "Sitemap of published articles",
        "description": "Machine-readable list of canonical article URLs on this host, with last-modified timestamps. Use this to enumerate every article before fetching individual pages.",
        "responses": {
          "200": {
            "description": "Sitemap XML document.",
            "content": {
              "application/xml": {
                "schema": {
                  "type": "string",
                  "description": "Sitemap 0.9 XML document."
                }
              }
            }
          }
        }
      }
    },
    "/llms.txt": {
      "get": {
        "operationId": "getLlmsTxt",
        "tags": ["Feeds"],
        "summary": "Agent guide for this site",
        "description": "Plain-text overview written for AI agents: what this site is, canonical URLs, authorship, and how to consume the content.",
        "responses": {
          "200": {
            "description": "Plain-text agent guide.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string",
                  "description": "Markdown-formatted guide served as text/plain."
                }
              }
            }
          }
        }
      }
    },
    "/api/posts/{slug}": {
      "get": {
        "operationId": "getPublishedPost",
        "tags": ["Articles"],
        "summary": "One published article as JSON",
        "description": "Returns the published article that matches the slug. Drafts and unknown slugs return 404 for anonymous callers. This is the same record the HTML page renders, so contentHtml matches the published body exactly.",
        "parameters": [
          {
            "$ref": "#/components/parameters/ArticleSlug"
          }
        ],
        "responses": {
          "200": {
            "description": "Published article.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Article"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFoundJson"
          }
        }
      }
    },
    "/{slug}": {
      "get": {
        "operationId": "getArticle",
        "tags": ["Articles"],
        "summary": "An article as HTML or Markdown",
        "description": "Renders the article page. Send `Accept: text/markdown` (or `text/x-markdown`) to receive a Markdown representation with the title, TL;DR, metadata, and canonical URL. Browsers receive HTML. Unknown slugs return 404: HTML clients get the styled page, Markdown clients get a Markdown body linking the sitemap, llms.txt, and API docs.",
        "parameters": [
          {
            "$ref": "#/components/parameters/ArticleSlug"
          },
          {
            "name": "Accept",
            "in": "header",
            "required": false,
            "description": "Set to `text/markdown` or `text/x-markdown` for the Markdown representation.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Article content in the requested representation.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string",
                  "description": "Rendered article page."
                }
              },
              "text/markdown": {
                "schema": {
                  "type": "string",
                  "description": "Markdown representation of the article."
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFoundMarkdown"
          }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "ArticleSlug": {
        "name": "slug",
        "in": "path",
        "required": true,
        "description": "Article slug: lowercase letters, digits, and hyphens. Enumerate current values from /sitemap.xml.",
        "schema": {
          "type": "string",
          "pattern": "^[a-z0-9-]+$",
          "maxLength": 300
        },
        "example": "ai-agent-sandboxing-guide-how-i-would-lock-up-an-ai-that-runs-code"
      }
    },
    "schemas": {
      "Article": {
        "type": "object",
        "description": "A published article as stored and rendered by this site.",
        "required": [
          "slug",
          "title",
          "tldr",
          "contentHtml",
          "status",
          "createdAt",
          "updatedAt",
          "readingTime",
          "tags"
        ],
        "properties": {
          "slug": {
            "type": "string",
            "pattern": "^[a-z0-9-]+$",
            "description": "Canonical slug. The article URL is https://blogs.tirup.in/{slug}."
          },
          "title": {
            "type": "string",
            "maxLength": 300,
            "description": "Human-facing title, used as the page H1."
          },
          "seoTitle": {
            "type": "string",
            "maxLength": 300,
            "description": "Optional crawler-facing title used in metadata and Open Graph tags."
          },
          "tldr": {
            "type": "string",
            "maxLength": 2000,
            "description": "Short summary shown in the TL;DR box."
          },
          "content": {
            "type": "string",
            "description": "TipTap document JSON, stringified."
          },
          "contentHtml": {
            "type": "string",
            "description": "Canonical rendered HTML body of the article."
          },
          "status": {
            "type": "string",
            "enum": ["published"],
            "description": "Anonymous callers only ever see published articles."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "Publication timestamp (ISO 8601)."
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "Last edit timestamp (ISO 8601)."
          },
          "readingTime": {
            "type": "integer",
            "minimum": 1,
            "maximum": 999,
            "description": "Estimated reading time in minutes."
          },
          "tags": {
            "type": "array",
            "maxItems": 20,
            "items": {
              "type": "string",
              "maxLength": 60
            },
            "description": "Topic tags."
          }
        }
      },
      "Error": {
        "type": "object",
        "description": "Typed error envelope returned by the JSON endpoints.",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable message. Machine handling should branch on the HTTP status code."
          }
        }
      }
    },
    "responses": {
      "NotFoundJson": {
        "description": "No published article exists at that slug.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "Not found"
            }
          }
        }
      },
      "NotFoundMarkdown": {
        "description": "No article or page exists at that path. Markdown clients receive a recovery body; HTML clients receive the 404 page.",
        "content": {
          "text/markdown": {
            "schema": {
              "type": "string",
              "description": "Markdown body naming the missing path and linking the sitemap, llms.txt, and API docs."
            }
          },
          "text/html": {
            "schema": {
              "type": "string",
              "description": "Styled 404 page."
            }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests. Retry after the number of seconds in the Retry-After header.",
        "headers": {
          "Retry-After": {
            "description": "Seconds to wait before retrying.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "Rate limited"
            }
          }
        }
      },
      "FeedUnavailable": {
        "description": "The article store could not be reached. Retry later.",
        "content": {
          "text/plain": {
            "schema": {
              "type": "string"
            },
            "example": "RSS feed unavailable"
          }
        }
      }
    }
  },
  "x-rate-limit-policy": "Public read endpoints are CDN-cached and not rate limited. The author-only write API allows 100 requests per minute per IP and returns 429 with Retry-After.",
  "x-deprecation-policy": "Version v1. Breaking changes are announced in https://blogs.tirup.in/llms.txt and at https://blogs.tirup.in/docs at least 90 days before taking effect. Retiring endpoints send Deprecation and Sunset headers during the notice window."
}
