{
  "openapi": "3.1.0",
  "info": {
    "title": "neurohackingly.com API",
    "version": "1.0.0",
    "summary": "Newsletter signup and machine-readable content endpoints of neurohackingly.com",
    "description": "neurohackingly.com is a personal blog written in English. Besides its HTML pages it exposes one write endpoint (newsletter signup) and several read-only, prerendered content endpoints intended for agents and feed readers. All content endpoints are static, cacheable, and require no authentication. The site is deliberately AI-friendly: robots.txt allows AI crawlers and links /llms.txt. API catalog (RFC 9727): https://neurohackingly.com/.well-known/api-catalog — A read-only MCP server is available at https://www.neurohackingly.com/mcp (see https://neurohackingly.com/.well-known/mcp/server-card.json).",
    "contact": {
      "name": "Lukas Zangerl",
      "url": "https://neurohackingly.com"
    }
  },
  "servers": [
    {
      "url": "https://www.neurohackingly.com",
      "description": "Canonical host"
    },
    {
      "url": "https://neurohackingly.com",
      "description": "Apex — redirects (307) to the canonical www host"
    }
  ],
  "paths": {
    "/api/subscribe": {
      "post": {
        "operationId": "subscribeToNewsletter",
        "summary": "Subscribe an email address to the newsletter",
        "description": "Adds a subscriber via ConvertKit; double opt-in is handled by ConvertKit (the subscriber receives a confirmation email). Content negotiation: if the request sends 'Accept: application/json' (or the header 'X-Requested-With: fetch'), the endpoint always answers HTTP 200 with a JSON body whose 'status' field carries the outcome. Otherwise (plain browser form post) it answers HTTP 303 with a Location redirect to /newsletter?status=success|already|error. Rate limit: 5 requests per minute per client IP (per warm serverless instance); exceeding it yields outcome 'error'. Only subscribe with the explicit consent of the person whose email address is used.",
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/SubscribeRequest"
              }
            },
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/SubscribeRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON mode (request sent 'Accept: application/json' or 'X-Requested-With: fetch'). The HTTP status is always 200; the outcome — including validation, rate-limit, and upstream errors — is reported in the 'status' field of the body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SubscribeResult"
                }
              }
            }
          },
          "303": {
            "description": "Browser form mode (no 'Accept: application/json'): redirect to the newsletter page with the outcome in the query string.",
            "headers": {
              "Location": {
                "description": "/newsletter?status=success|already|error",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/llms.txt": {
      "get": {
        "operationId": "getLlmsTxt",
        "summary": "llms.txt index (llmstxt.org format)",
        "description": "Markdown index of the site for LLM agents: H1 title, summary blockquote, sectioned link lists of all published posts, and an 'Optional' section. Post links point at the markdown mirrors (/{slug}.md), so agents get clean markdown instead of HTML.",
        "responses": {
          "200": {
            "description": "llms.txt document",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/llms-full.txt": {
      "get": {
        "operationId": "getLlmsFullTxt",
        "summary": "Full content of all posts in a single markdown file",
        "description": "Concatenated clean-markdown content of every published post, suitable for one-shot ingestion by an LLM agent.",
        "responses": {
          "200": {
            "description": "llms-full.txt document",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/rss.xml": {
      "get": {
        "operationId": "getRssFeed",
        "summary": "RSS 2.0 feed",
        "description": "RSS 2.0 feed of all published posts.",
        "responses": {
          "200": {
            "description": "RSS 2.0 feed document",
            "content": {
              "application/xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/{slug}.md": {
      "get": {
        "operationId": "getPostMarkdown",
        "summary": "Markdown mirror of a blog post",
        "description": "Every published post is available as clean Markdown at its canonical URL plus '.md'. The list of all mirror links is in /llms.txt. The homepage is mirrored at /index.md. Requesting a post URL (or /) with 'Accept: text/markdown' also returns the markdown representation.",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Post slug. Discover valid slugs via /llms.txt or /rss.xml.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Markdown source of the post",
            "content": {
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Unknown slug (static HTML error page)",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "SubscribeRequest": {
        "type": "object",
        "required": [
          "email"
        ],
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "description": "Subscriber email address (required)."
          },
          "name": {
            "type": "string",
            "description": "Optional first name."
          },
          "source": {
            "type": "string",
            "description": "Optional free-text signup source, stored as a ConvertKit custom field for segmentation."
          },
          "website": {
            "type": "string",
            "maxLength": 0,
            "description": "Honeypot spam trap. MUST be omitted or sent empty; any non-empty value causes outcome 'error'."
          }
        }
      },
      "SubscribeResult": {
        "type": "object",
        "required": [
          "status"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "success",
              "already",
              "error"
            ],
            "description": "'success' = subscribed (confirmation email sent), 'already' = this email is already subscribed, 'error' = validation, rate-limit, configuration, or upstream failure."
          },
          "code": {
            "type": "integer",
            "description": "Present on some errors: HTTP status of the upstream or internal failure."
          },
          "error": {
            "type": "string",
            "description": "Present on some errors: human-readable message."
          }
        }
      }
    }
  }
}
