{
  "openapi": "3.1.0",
  "info": {
    "title": "markgibbons.dev API",
    "summary": "Public, read-only JSON access to the posts on markgibbons.dev.",
    "description": "Read the archive of markgibbons.dev - writing on Sitecore, SitecoreAI, headless architecture, Model Context Protocol, and AI in delivery teams - as JSON. No authentication, no API key, no quota. Every response is public and cacheable. Errors are RFC 9457 problem documents with a stable `code` and a `resolution` hint.\n\nThe same content is also available as markdown at every page URL (send `Accept: text/markdown`, or append `.md` to the path), as an MCP server at https://www.markgibbons.dev/mcp, and as llms.txt at https://www.markgibbons.dev/llms.txt.\n\n## Rate limits\n\nResponses carry `RateLimit-Policy` and `RateLimit` (IETF structured fields) plus the older `X-RateLimit-*` headers. A `429` carries `Retry-After`. Enforcement is best-effort and per serverless instance, so the real ceiling across the fleet is a multiple of the advertised quota; read the headers and self-throttle rather than assuming a hard limit.\n\n## Versioning and deprecation\n\nThe major version is in the URL path (`/api/v1`). A breaking change gets a new path; nothing breaking is ever made to `v1` in place. Additive changes - new fields, new optional parameters, new endpoints - can land in `v1` without notice, so ignore fields you do not recognise. Before a version is retired its responses carry the RFC 8594 `Sunset` header and a `Deprecation` header, and the policy is restated at https://www.markgibbons.dev/developers. The minimum notice is six months.",
    "version": "1.0.0",
    "contact": {
      "name": "Mark Gibbons",
      "url": "https://www.markgibbons.dev/contact"
    },
    "license": {
      "name": "Content is the author's own; the site source is MIT",
      "url": "https://github.com/markgibbons25/blog/blob/main/LICENSE"
    }
  },
  "externalDocs": {
    "description": "Developer and agent documentation for markgibbons.dev",
    "url": "https://www.markgibbons.dev/developers"
  },
  "servers": [
    {
      "url": "https://www.markgibbons.dev/api/v1",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "site",
      "description": "Who runs the site and what it covers."
    },
    {
      "name": "posts",
      "description": "The published archive."
    },
    {
      "name": "tags",
      "description": "Topics, with post counts."
    }
  ],
  "paths": {
    "/site": {
      "get": {
        "operationId": "getSite",
        "tags": ["site"],
        "summary": "Get site information",
        "description": "Who runs markgibbons.dev, what it covers, how many posts it has, and where every machine-readable surface of the site lives. Call this first when deciding whether the site is relevant to a question.",
        "responses": {
          "200": {
            "description": "Site information.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Site"
                }
              }
            }
          },
          "400": {
            "description": "A query parameter was out of range or the wrong type.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such post, tag, or endpoint.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "The API is read-only; only GET, HEAD and OPTIONS are supported.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "The client exceeded the advertised quota. Carries Retry-After.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "Something failed on the server.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/posts": {
      "get": {
        "operationId": "listPosts",
        "tags": ["posts"],
        "summary": "List and search posts",
        "description": "Published posts, newest first. Narrow by tag, search with `q`, and page with `limit` and `offset`. Post bodies are omitted here; call getPost with a slug to read one in full.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Full-text search. Every word must appear somewhere in the post; matches in the title or summary rank higher.",
            "schema": {
              "type": "string"
            },
            "example": "sitecoreai migration"
          },
          {
            "name": "tag",
            "in": "query",
            "required": false,
            "description": "Restrict to one tag. Use the tag slug from listTags.",
            "schema": {
              "type": "string"
            },
            "example": "sitecore"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum posts to return.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of posts to skip, for paging.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 10000,
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of posts.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PostList"
                }
              }
            }
          },
          "400": {
            "description": "A query parameter was out of range or the wrong type.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such post, tag, or endpoint.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "The API is read-only; only GET, HEAD and OPTIONS are supported.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "The client exceeded the advertised quota. Carries Retry-After.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "Something failed on the server.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/posts/{year}/{slug}": {
      "get": {
        "operationId": "getPost",
        "tags": ["posts"],
        "summary": "Get one post in full",
        "description": "One published post, including its full markdown body. Slugs come from listPosts and are the part of the post URL after /blog/ - a year and a name.",
        "parameters": [
          {
            "name": "year",
            "in": "path",
            "required": true,
            "description": "The post's publication year, the first segment of its slug.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]{4}$"
            },
            "example": "2026"
          },
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The name part of the slug, e.g. \"llm-cheat-sheet\".",
            "schema": {
              "type": "string"
            },
            "example": "llm-cheat-sheet"
          }
        ],
        "responses": {
          "200": {
            "description": "The post, with its markdown body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PostDetail"
                }
              }
            }
          },
          "400": {
            "description": "A query parameter was out of range or the wrong type.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such post, tag, or endpoint.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "The API is read-only; only GET, HEAD and OPTIONS are supported.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "The client exceeded the advertised quota. Carries Retry-After.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "Something failed on the server.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/tags": {
      "get": {
        "operationId": "listTags",
        "tags": ["tags"],
        "summary": "List tags",
        "description": "Every topic covered on the site, with its post count, most-used first. Use a tag slug to narrow listPosts.",
        "responses": {
          "200": {
            "description": "All tags.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TagList"
                }
              }
            }
          },
          "400": {
            "description": "A query parameter was out of range or the wrong type.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such post, tag, or endpoint.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "The API is read-only; only GET, HEAD and OPTIONS are supported.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "The client exceeded the advertised quota. Carries Retry-After.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "Something failed on the server.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        },
        "parameters": [
          {
            "name": "min_posts",
            "in": "query",
            "required": false,
            "description": "Only return tags with at least this many posts. Use it to drop one-off tags.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 1
            },
            "example": 3
          }
        ]
      }
    },
    "/posts/{year}/{slug}/related": {
      "get": {
        "operationId": "getRelatedPosts",
        "tags": ["posts"],
        "summary": "Get posts related to one post",
        "description": "Other published posts that share tags with this one, most tags in common first. Use it to follow a topic without guessing at search terms.",
        "parameters": [
          {
            "name": "year",
            "in": "path",
            "required": true,
            "description": "The post's publication year, the first segment of its slug.",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]{4}$"
            },
            "example": "2026"
          },
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The post to find neighbours for.",
            "schema": {
              "type": "string"
            },
            "example": "llm-cheat-sheet"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum related posts to return.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 25,
              "default": 5
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Related posts, most tags in common first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RelatedPostList"
                }
              }
            }
          },
          "400": {
            "description": "A query parameter was out of range or the wrong type.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "404": {
            "description": "No such post, tag, or endpoint.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "405": {
            "description": "The API is read-only; only GET, HEAD and OPTIONS are supported.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "The client exceeded the advertised quota. Carries Retry-After.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "500": {
            "description": "Something failed on the server.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Post": {
        "type": "object",
        "description": "One published post, without its body.",
        "required": ["slug", "title", "url", "published", "tags"],
        "properties": {
          "slug": {
            "type": "string",
            "description": "Stable identifier, e.g. \"2026/llm-cheat-sheet\"."
          },
          "title": {
            "type": "string",
            "description": "Post title."
          },
          "summary": {
            "type": ["string", "null"],
            "description": "One-paragraph summary, or null if the post has none."
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Canonical HTML URL."
          },
          "markdown_url": {
            "type": "string",
            "format": "uri",
            "description": "The same post as markdown."
          },
          "published": {
            "type": "string",
            "format": "date-time",
            "description": "Publication date, ISO 8601."
          },
          "updated": {
            "type": "string",
            "format": "date-time",
            "description": "Last modification date, ISO 8601. Equals `published` if never revised."
          },
          "tags": {
            "type": "array",
            "description": "Topics this post is filed under.",
            "items": {
              "type": "string"
            }
          },
          "reading_minutes": {
            "type": "integer",
            "description": "Estimated reading time in minutes."
          },
          "author": {
            "type": "string",
            "description": "Author name."
          }
        }
      },
      "PostDetail": {
        "type": "object",
        "description": "One published post, including its markdown body.",
        "allOf": [
          {
            "$ref": "#/components/schemas/Post"
          }
        ],
        "properties": {
          "content": {
            "type": "string",
            "description": "The full post body, as markdown."
          }
        }
      },
      "PostList": {
        "type": "object",
        "description": "A page of posts.",
        "required": ["total", "limit", "offset", "items"],
        "properties": {
          "total": {
            "type": "integer",
            "description": "Posts matching the query, before paging."
          },
          "limit": {
            "type": "integer",
            "description": "The limit that was applied."
          },
          "offset": {
            "type": "integer",
            "description": "The offset that was applied."
          },
          "items": {
            "type": "array",
            "description": "The posts on this page, newest first.",
            "items": {
              "$ref": "#/components/schemas/Post"
            }
          }
        }
      },
      "Tag": {
        "type": "object",
        "description": "One topic.",
        "required": ["tag", "post_count", "url"],
        "properties": {
          "tag": {
            "type": "string",
            "description": "Tag slug, usable as the `tag` parameter."
          },
          "post_count": {
            "type": "integer",
            "description": "Posts filed under this tag."
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "The tag's page on the site."
          }
        }
      },
      "TagList": {
        "type": "object",
        "description": "Every tag on the site.",
        "required": ["total", "items"],
        "properties": {
          "total": {
            "type": "integer",
            "description": "Number of tags."
          },
          "items": {
            "type": "array",
            "description": "Tags, most-used first.",
            "items": {
              "$ref": "#/components/schemas/Tag"
            }
          }
        }
      },
      "Site": {
        "type": "object",
        "description": "Who runs the site and where its machine-readable surfaces are.",
        "required": ["name", "description", "url", "post_count"],
        "properties": {
          "name": {
            "type": "string",
            "description": "Site name."
          },
          "description": {
            "type": "string",
            "description": "One-line description."
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Site home page."
          },
          "language": {
            "type": "string",
            "description": "Content language tag."
          },
          "author_name": {
            "type": "string",
            "description": "Who writes the site."
          },
          "author_role": {
            "type": ["string", "null"],
            "description": "The author's role."
          },
          "author_company": {
            "type": ["string", "null"],
            "description": "The author's employer."
          },
          "author_location": {
            "type": "string",
            "description": "City, region and country."
          },
          "post_count": {
            "type": "integer",
            "description": "Published posts."
          },
          "tag_count": {
            "type": "integer",
            "description": "Distinct tags."
          },
          "latest_post_published": {
            "type": ["string", "null"],
            "format": "date-time",
            "description": "When the newest post went out."
          },
          "topics": {
            "type": "array",
            "description": "What the site is a good source for.",
            "items": {
              "type": "string"
            }
          },
          "openapi_url": {
            "type": "string",
            "format": "uri",
            "description": "This document."
          },
          "mcp_url": {
            "type": "string",
            "format": "uri",
            "description": "MCP server, Streamable HTTP, no authentication."
          },
          "llms_txt_url": {
            "type": "string",
            "format": "uri",
            "description": "Agent index."
          },
          "llms_full_txt_url": {
            "type": "string",
            "format": "uri",
            "description": "Every post in one file."
          },
          "feed_url": {
            "type": "string",
            "format": "uri",
            "description": "RSS feed."
          },
          "sitemap_url": {
            "type": "string",
            "format": "uri",
            "description": "XML sitemap."
          },
          "documentation_url": {
            "type": "string",
            "format": "uri",
            "description": "Human-readable developer docs."
          },
          "contact_url": {
            "type": "string",
            "format": "uri",
            "description": "How to reach the author."
          }
        }
      },
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem details. Branch on `code`; act on `resolution`.",
        "required": ["type", "title", "status", "code", "resolution"],
        "properties": {
          "type": {
            "type": "string",
            "format": "uri",
            "description": "Problem type identifier."
          },
          "title": {
            "type": "string",
            "description": "Short, human-readable summary."
          },
          "status": {
            "type": "integer",
            "description": "HTTP status code."
          },
          "detail": {
            "type": "string",
            "description": "What went wrong with this request."
          },
          "code": {
            "type": "string",
            "description": "Stable machine-readable error code.",
            "enum": [
              "invalid_parameter",
              "post_not_found",
              "tag_not_found",
              "unknown_endpoint",
              "method_not_allowed",
              "rate_limited"
            ]
          },
          "resolution": {
            "type": "string",
            "description": "What to do about it."
          },
          "parameter": {
            "type": "string",
            "description": "The query parameter at fault, when there is one."
          },
          "documentation": {
            "type": "string",
            "format": "uri",
            "description": "Developer docs."
          },
          "retry_after": {
            "type": "integer",
            "description": "Seconds to wait before retrying, on a 429."
          }
        }
      },
      "RelatedPost": {
        "type": "object",
        "description": "A post that shares tags with another.",
        "allOf": [
          {
            "$ref": "#/components/schemas/Post"
          }
        ],
        "properties": {
          "shared_tags": {
            "type": "integer",
            "description": "How many tags it has in common."
          }
        }
      },
      "RelatedPostList": {
        "type": "object",
        "description": "Posts related to one post.",
        "required": ["slug", "total", "limit", "items"],
        "properties": {
          "slug": {
            "type": "string",
            "description": "The post the list is relative to."
          },
          "total": {
            "type": "integer",
            "description": "Related posts found, before the limit."
          },
          "limit": {
            "type": "integer",
            "description": "The limit that was applied."
          },
          "items": {
            "type": "array",
            "description": "Related posts.",
            "items": {
              "$ref": "#/components/schemas/RelatedPost"
            }
          }
        }
      }
    }
  }
}
