{
  "openapi": "3.1.0",
  "info": {
    "title": "Joel Saji API",
    "version": "1.0.0",
    "summary": "Read-only JSON access to joelcsaji.com content.",
    "description": "Public, unauthenticated, read-only endpoints for the content published on joelcsaji.com: articles, portfolio case studies, and the AI dictionary.\n\nNo API key is required. Every response is JSON with `Access-Control-Allow-Origin: *`.\n\nRequests are limited to 60 per 60 seconds per client, reported through the `RateLimit` and `RateLimit-Policy` headers (draft-ietf-httpapi-ratelimit-headers); a 429 carries `Retry-After`.\n\nErrors always use the `Error` schema: a machine-readable `code`, a human-readable `message`, and a `hint` describing how to resolve the problem.\n\nSuccessful responses carry an `ETag`: repeat the request with that value in `If-None-Match` and an unchanged resource answers `304 Not Modified` with no body.\n\nEvery HTML page on the site is also available as Markdown by sending `Accept: text/markdown`.",
    "contact": {
      "name": "Joel Saji",
      "url": "https://www.joelcsaji.com/contact",
      "email": "hello@joelcsaji.com"
    },
    "license": {
      "name": "Content © Joel Saji. Cite the canonical URL.",
      "identifier": "LicenseRef-Site-Content"
    }
  },
  "externalDocs": {
    "description": "Developer portal",
    "url": "https://www.joelcsaji.com/developers"
  },
  "servers": [
    {
      "url": "https://www.joelcsaji.com"
    }
  ],
  "tags": [
    {
      "name": "posts",
      "description": "Articles published on the blog."
    },
    {
      "name": "projects",
      "description": "Portfolio projects and case studies."
    },
    {
      "name": "now",
      "description": "What Joel is focused on right now, from the /now page."
    },
    {
      "name": "ai-terms",
      "description": "Entries from the AI dictionary."
    },
    {
      "name": "search",
      "description": "The site-wide search index behind the command palette."
    }
  ],
  "paths": {
    "/api/posts": {
      "get": {
        "operationId": "listPosts",
        "tags": [
          "posts"
        ],
        "summary": "List published articles",
        "description": "Returns published articles, newest first, without the article body. Narrow the list with `q`, `tag`, and `category` and page through it with `page` and `perPage`; without parameters every article is returned. Fetch `/api/posts/{slug}` for the Markdown content.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Free-text match against the title, description, and full article body, the same search the blog index runs. Throws away leading and trailing whitespace and is capped at 120 characters.",
            "schema": {
              "type": "string",
              "maxLength": 120
            }
          },
          {
            "name": "tag",
            "in": "query",
            "required": false,
            "description": "Only return articles carrying this tag. Comma-separate values (`?tag=react,typescript`) to match any of several, exactly as the blog index does: each value is trimmed and lowercased before matching, and a value that carries no facet is rejected with `invalid_parameter`. Unknown tags return an empty list.",
            "schema": {
              "type": "string",
              "pattern": "^(?=.*[A-Za-z0-9])\\s*(?:[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)?\\s*(?:,\\s*(?:[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)?\\s*)*$"
            }
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "description": "Only return articles in this category, using the slug values this site publishes (for example `ai`). Comma-separate to match any of several; each value is trimmed and lowercased before matching, and a value that carries no facet is rejected. Unknown categories are rejected with `invalid_parameter` and a hint listing the categories this site publishes.",
            "schema": {
              "type": "string",
              "pattern": "^(?=.*[A-Za-z0-9])\\s*(?:[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)?\\s*(?:,\\s*(?:[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)?\\s*)*$"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "1-based page of results. A page beyond `totalPages` returns an empty `items` list.",
            "schema": {
              "type": "integer",
              "minimum": 1
            }
          },
          {
            "name": "perPage",
            "in": "query",
            "required": false,
            "description": "Items per page. Defaults to 20, capped at 50.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Published articles, newest first. `page`, `perPage`, and `totalPages` appear when paging parameters are requested; `filters` echoes the `q`, `tag`, `category`, `page`, and `perPage` that were applied.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "type": "object",
                      "required": [
                        "items",
                        "total"
                      ],
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/PostSummary"
                          }
                        },
                        "total": {
                          "type": "integer",
                          "description": "Number of items the filters matched, not the length of `items`. Without paging parameters the two are equal."
                        }
                      }
                    },
                    {
                      "type": "object",
                      "properties": {
                        "page": {
                          "type": "integer",
                          "minimum": 1,
                          "description": "The 1-based page these `items` came from."
                        },
                        "perPage": {
                          "type": "integer",
                          "minimum": 1,
                          "maximum": 50,
                          "description": "Page size in effect for this response."
                        },
                        "totalPages": {
                          "type": "integer",
                          "minimum": 0,
                          "description": "Pages available at this `perPage`."
                        },
                        "filters": {
                          "type": "object",
                          "description": "The filters that were applied, each reported as it was read rather than as it was sent. Facets keep the comma-separated spelling this endpoint accepts.",
                          "properties": {
                            "q": {
                              "type": "string",
                              "maxLength": 120
                            },
                            "tag": {
                              "type": "string"
                            },
                            "category": {
                              "type": "string"
                            },
                            "page": {
                              "type": "integer",
                              "minimum": 1
                            },
                            "perPage": {
                              "type": "integer",
                              "minimum": 1,
                              "maximum": 50
                            }
                          }
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "items": [
                    {
                      "slug": "ios-27-ipados-27-macos-27-watchos-27-tvos-27",
                      "title": "iOS 27, iPadOS 27, macOS 27, watchOS 27 and tvOS 27",
                      "description": "Every change in iOS 27, iPadOS 27, macOS 27, watchOS 27 and tvOS 27: Siri AI, the Mac redesign, new APIs, deprecations and what did not ship.",
                      "author": {
                        "name": "Joel Saji"
                      },
                      "category": "Mobile Development",
                      "tags": [
                        {
                          "label": "Apple",
                          "value": "apple"
                        },
                        {
                          "label": "iOS",
                          "value": "ios"
                        },
                        {
                          "label": "macOS",
                          "value": "macos"
                        },
                        {
                          "label": "Apple Intelligence",
                          "value": "apple-intelligence"
                        }
                      ],
                      "publishedAt": "2026-09-14T12:00:00.000Z",
                      "readTimeMinutes": 13,
                      "keywords": [],
                      "url": "https://www.joelcsaji.com/blog/ios-27-ipados-27-macos-27-watchos-27-tvos-27"
                    },
                    {
                      "slug": "fable-5-export-controls-and-glm-5-2",
                      "title": "Fable 5, Export Controls, and GLM 5.2",
                      "description": "Anthropic's Fable 5 hit US export controls days after launch. GLM 5.2 shipped open weights in the same window. What builders should take from both.",
                      "author": {
                        "name": "Joel Saji"
                      },
                      "category": "Artificial Intelligence",
                      "tags": [
                        {
                          "label": "AI",
                          "value": "ai"
                        },
                        {
                          "label": "Claude",
                          "value": "claude"
                        },
                        {
                          "label": "Open Source",
                          "value": "open-source"
                        }
                      ],
                      "publishedAt": "2026-07-16T12:00:00.000Z",
                      "readTimeMinutes": 8,
                      "keywords": [],
                      "url": "https://www.joelcsaji.com/blog/fable-5-export-controls-and-glm-5-2"
                    }
                  ],
                  "total": 20,
                  "page": 1,
                  "perPage": 2,
                  "totalPages": 10,
                  "filters": {
                    "page": 1,
                    "perPage": 2
                  }
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/posts/{slug}": {
      "get": {
        "operationId": "getPost",
        "tags": [
          "posts"
        ],
        "summary": "Get one article",
        "description": "Returns a single published article including its full body as Markdown.",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Article slug as listed by `listPosts`.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
              "maxLength": 120
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The article.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Post"
                },
                "example": {
                  "slug": "react-19-deep-dive-server-components-compiler-hooks",
                  "title": "React 19 Deep Dive: Server Components, Compiler, and the New Hooks",
                  "description": "React 19 in practice: Server Components, the Compiler, and hooks like useOptimistic, useActionState, and use().",
                  "author": {
                    "name": "Joel Saji"
                  },
                  "category": "Web Development",
                  "tags": [
                    {
                      "label": "React",
                      "value": "react"
                    },
                    {
                      "label": "JavaScript",
                      "value": "javascript"
                    },
                    {
                      "label": "Frontend",
                      "value": "frontend"
                    }
                  ],
                  "publishedAt": "2025-10-15T21:00:00.000Z",
                  "readTimeMinutes": 8,
                  "keywords": [],
                  "url": "https://www.joelcsaji.com/blog/react-19-deep-dive-server-components-compiler-hooks",
                  "content": "React 19 represents the most significant evolution of the library since hooks were introduced in React 16.8. Released in December 2024, and followed by React 19.2 in October 2025, this release fundamentally changes how we build React applications. In this article, we'll explore the three pillars of React 19: Server Components, the Compiler, and the new Hooks."
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/projects": {
      "get": {
        "operationId": "listProjects",
        "tags": [
          "projects"
        ],
        "summary": "List portfolio projects",
        "description": "Returns the featured portfolio projects with their case studies (the same curated set as the portfolio page).",
        "responses": {
          "200": {
            "description": "Portfolio projects.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "items",
                    "total"
                  ],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Project"
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Number of items the filters matched, not the length of `items`. Without paging parameters the two are equal."
                    }
                  }
                },
                "example": {
                  "items": [
                    {
                      "id": "church-grid",
                      "title": "Church Grid",
                      "description": "A church operations platform for members, events, communication, donations, and reporting.",
                      "seoDescription": "Church Grid is a church operations platform by Toronto software developer Joel Saji, bringing members, events, giving, and reporting into one calm tool.",
                      "technologies": [
                        "Next.js",
                        "React",
                        "TypeScript",
                        "Tailwind CSS",
                        "PostgreSQL",
                        "Auth.js"
                      ],
                      "liveUrl": "https://www.churchgrid.app",
                      "featured": true,
                      "role": "Founder & Solo Developer",
                      "timeline": "2024 to Present",
                      "caseStudy": {
                        "sections": [
                          {
                            "title": "Problem",
                            "body": "Churches juggle spreadsheets for members, separate tools for events, and manual donation workflows. Nothing ties operations together."
                          },
                          {
                            "title": "Constraints",
                            "body": "Staff are often non-technical and need a calm UI. Member and financial data also need appropriate security."
                          },
                          {
                            "title": "Solution",
                            "body": "An all-in-one platform: member directory, event scheduling with RSVP, communication tools, and secure giving with reporting, built for ease of use."
                          }
                        ],
                        "architecture": "Next.js full-stack app with role-based access, a relational database for members and events, secure payment processing with tax receipts, and a real-time layer for announcements.",
                        "outcome": "Live production product that brings core church operations into one connected workflow.",
                        "results": [
                          "Unified member, event, communication, giving, and reporting",
                          "Less reliance on spreadsheets and disconnected tools",
                          "Automated donation and reporting tasks where accuracy matters"
                        ],
                        "highlights": [
                          "Member directory with family grouping and roles",
                          "Events with reminders and RSVP tracking",
                          "Donations with tax receipt generation",
                          "Communication hub for announcements and follow-ups"
                        ],
                        "lessonsLearned": [
                          "Non-technical users need fewer options, not more docs",
                          "Consolidating tools compounds value when data flows between features",
                          "Trust matters more than feature velocity for community tools"
                        ]
                      },
                      "url": "https://www.joelcsaji.com/portfolio/church-grid"
                    },
                    {
                      "id": "moment-gallery",
                      "title": "Moment Gallery",
                      "description": "A photography portfolio and inquiry site for Ontario event, portrait, corporate, and food photography.",
                      "seoDescription": "Moment Gallery is an Ontario photography portfolio and inquiry site by Joel Saji, covering events, portraits, corporate, and food work with fast booking paths.",
                      "technologies": [
                        "TanStack Start",
                        "React",
                        "TypeScript",
                        "Responsive Design",
                        "Cloudflare Workers"
                      ],
                      "liveUrl": "https://moment.gallery",
                      "featured": true,
                      "role": "Founder & Solo Developer",
                      "timeline": "2026",
                      "caseStudy": {
                        "sections": [
                          {
                            "title": "Problem",
                            "body": "A working photographer needed a site that shows craft, services, availability, and client galleries, without looking like a generic template."
                          },
                          {
                            "title": "Constraints",
                            "body": "Editorial and high-end, but still fast, accessible, and easy to scan on mobile."
                          },
                          {
                            "title": "Solution",
                            "body": "A gallery-led interface with strong typography, clear service paths, structured metadata, and direct inquiry flows."
                          }
                        ],
                        "architecture": "TanStack Start and React with static-friendly routing, responsive images, structured SEO, and Cloudflare Workers delivery.",
                        "outcome": "Polished photography site that balances browsing, service positioning, and inquiry conversion.",
                        "results": [
                          "Memorable first screen around positioning and booking",
                          "Responsive gallery and service sections that keep images prominent",
                          "SEO and structured data for Ontario photography services"
                        ],
                        "highlights": [
                          "Editorial visual system with a gallery-led layout",
                          "Mobile navigation and inquiry paths for quick scanning",
                          "Service paths for events, portraits, corporate, and food",
                          "Client gallery entry in main navigation"
                        ],
                        "lessonsLearned": [
                          "Strong art direction still needs disciplined image sizing",
                          "Clear service taxonomy converts without heavy sales copy",
                          "Large typography needs dedicated mobile composition"
                        ]
                      },
                      "url": "https://www.joelcsaji.com/portfolio/moment-gallery"
                    },
                    {
                      "id": "waveform-apps",
                      "title": "Waveform Apps",
                      "description": "Custom AI-powered web applications for teams that need speed, clean integration, and practical guidance.",
                      "seoDescription": "Waveform Apps builds custom AI-powered web apps for small teams: fast delivery, clean integrations, and practical guidance from Toronto developer Joel Saji.",
                      "technologies": [
                        "Next.js",
                        "React",
                        "TypeScript",
                        "Tailwind CSS",
                        "AI Integration",
                        "Cloudflare Workers"
                      ],
                      "liveUrl": "https://www.waveformapps.com",
                      "featured": true,
                      "role": "Founder & Solo Developer",
                      "timeline": "2023 to Present",
                      "caseStudy": {
                        "sections": [
                          {
                            "title": "Problem",
                            "body": "Small teams struggle to ship polished web products quickly. Agencies are slow and expensive; DIY often lacks polish and scale."
                          },
                          {
                            "title": "Constraints",
                            "body": "Clients needed launch-ready products in 4 to 8 weeks, often with limited technical resources and shifting requirements."
                          },
                          {
                            "title": "Solution",
                            "body": "A consultancy model around composable patterns, AI-assisted workflows, and reusable components to shorten onboarding and ship production-ready products faster."
                          }
                        ],
                        "architecture": "Next.js and TanStack Start on Cloudflare Workers with edge-optimized builds, a shared Tailwind component library, and reviewed AI-assisted workflows in the pipeline.",
                        "outcome": "Client products from concept to launch, with a repeatable process for discovery, focused builds, and clean handoff.",
                        "results": [
                          "Faster discovery via reusable briefs and technical baselines",
                          "Less repetitive implementation with reviewed AI-assisted workflows",
                          "Performance budgets and deployment hygiene across client work"
                        ],
                        "highlights": [
                          "Composable component library that speeds client onboarding",
                          "AI-assisted workflows that cut boilerplate meaningfully",
                          "Production deploys on Cloudflare Workers with edge-optimized builds",
                          "Consistent design system across engagements"
                        ],
                        "lessonsLearned": [
                          "Onboarding checklists prevent scope creep better than contracts alone",
                          "Reusable libraries compound in value with each engagement",
                          "AI workflows work best with clear code review"
                        ]
                      },
                      "url": "https://www.joelcsaji.com/portfolio/waveform-apps"
                    }
                  ],
                  "total": 3
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/projects/{id}": {
      "get": {
        "operationId": "getProject",
        "tags": [
          "projects"
        ],
        "summary": "Get one project",
        "description": "Returns a single portfolio project and its case study.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Project id. Unlisted projects stay fetchable by direct id even though `listProjects` omits them.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
              "maxLength": 120
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The project.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Project"
                },
                "example": {
                  "id": "church-grid",
                  "title": "Church Grid",
                  "description": "A church operations platform for members, events, communication, donations, and reporting.",
                  "seoDescription": "Church Grid is a church operations platform by Toronto software developer Joel Saji, bringing members, events, giving, and reporting into one calm tool.",
                  "technologies": [
                    "Next.js",
                    "React",
                    "TypeScript",
                    "Tailwind CSS",
                    "PostgreSQL",
                    "Auth.js"
                  ],
                  "liveUrl": "https://www.churchgrid.app",
                  "featured": true,
                  "role": "Founder & Solo Developer",
                  "timeline": "2024 to Present",
                  "caseStudy": {
                    "sections": [
                      {
                        "title": "Problem",
                        "body": "Churches juggle spreadsheets for members, separate tools for events, and manual donation workflows. Nothing ties operations together."
                      },
                      {
                        "title": "Constraints",
                        "body": "Staff are often non-technical and need a calm UI. Member and financial data also need appropriate security."
                      },
                      {
                        "title": "Solution",
                        "body": "An all-in-one platform: member directory, event scheduling with RSVP, communication tools, and secure giving with reporting, built for ease of use."
                      }
                    ],
                    "architecture": "Next.js full-stack app with role-based access, a relational database for members and events, secure payment processing with tax receipts, and a real-time layer for announcements.",
                    "outcome": "Live production product that brings core church operations into one connected workflow.",
                    "results": [
                      "Unified member, event, communication, giving, and reporting",
                      "Less reliance on spreadsheets and disconnected tools",
                      "Automated donation and reporting tasks where accuracy matters"
                    ],
                    "highlights": [
                      "Member directory with family grouping and roles",
                      "Events with reminders and RSVP tracking",
                      "Donations with tax receipt generation",
                      "Communication hub for announcements and follow-ups"
                    ],
                    "lessonsLearned": [
                      "Non-technical users need fewer options, not more docs",
                      "Consolidating tools compounds value when data flows between features",
                      "Trust matters more than feature velocity for community tools"
                    ]
                  },
                  "url": "https://www.joelcsaji.com/portfolio/church-grid"
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/now": {
      "get": {
        "operationId": "getNow",
        "tags": [
          "now"
        ],
        "summary": "Get the /now snapshot",
        "description": "Returns the label/value rows the /now page and the homepage At-a-glance cards display, with the date the snapshot was last revised and the canonical page URL. The snapshot is revised by hand, so `lastUpdated` moves only when the content does.",
        "responses": {
          "200": {
            "description": "The current snapshot.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NowSnapshot"
                },
                "example": {
                  "lastUpdated": "2026-09-10",
                  "items": [
                    {
                      "label": "Now",
                      "value": "Church Grid & Waveform Apps"
                    },
                    {
                      "label": "Studying",
                      "value": "AI agents & TanStack Start"
                    },
                    {
                      "label": "Base",
                      "value": "Toronto, Canada"
                    }
                  ],
                  "url": "https://www.joelcsaji.com/now"
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/ai-terms": {
      "get": {
        "operationId": "searchAiTerms",
        "tags": [
          "ai-terms"
        ],
        "summary": "Search the AI dictionary",
        "description": "Returns dictionary entries matching an optional free-text query, category, and maturity filter. Results are compact rows; fetch `/api/ai-terms/{slug}` for the full entry.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Free-text query matched against the term name, slug, and aliases only. Longer values are truncated to 120 characters.",
            "schema": {
              "type": "string",
              "maxLength": 120
            }
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "description": "Only return entries in this category.",
            "schema": {
              "type": "string",
              "enum": [
                "Agents & Autonomy",
                "Context & Memory",
                "Tools & Protocols",
                "Retrieval & Grounding",
                "Reasoning & Inference",
                "Evaluation & Safety",
                "Models & Training",
                "AI-Native Product"
              ]
            }
          },
          {
            "name": "maturity",
            "in": "query",
            "required": false,
            "description": "Only return entries at this adoption stage.",
            "schema": {
              "type": "string",
              "enum": [
                "Established",
                "Rising",
                "Emerging",
                "Watch"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Matching dictionary entries.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "type": "object",
                      "required": [
                        "items",
                        "total"
                      ],
                      "properties": {
                        "items": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/AiTermSummary"
                          }
                        },
                        "total": {
                          "type": "integer",
                          "description": "Number of items the filters matched, not the length of `items`. Without paging parameters the two are equal."
                        }
                      }
                    },
                    {
                      "type": "object",
                      "required": [
                        "query"
                      ],
                      "properties": {
                        "query": {
                          "type": "object",
                          "description": "The filters that were applied.",
                          "properties": {
                            "q": {
                              "type": "string"
                            },
                            "category": {
                              "type": "string"
                            },
                            "maturity": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "items": [
                    {
                      "slug": "agent-harness",
                      "term": "Agent Harness",
                      "aliases": [
                        "Harness",
                        "Agent runtime"
                      ],
                      "category": "Agents & Autonomy",
                      "maturity": "Rising",
                      "shortDefinition": "The runtime around a model that supplies tools, state, prompts, permissions, logs, and execution loops."
                    },
                    {
                      "slug": "harness-engineering",
                      "term": "Harness Engineering",
                      "aliases": [
                        "Agent harness engineering",
                        "Harness design"
                      ],
                      "category": "Agents & Autonomy",
                      "maturity": "Rising",
                      "shortDefinition": "The practice of designing, testing, and simplifying the runtime around an agent so it can work reliably across tools, sessions, and tasks."
                    },
                    {
                      "slug": "brain-hands-decoupling",
                      "term": "Brain-Hands Decoupling",
                      "aliases": [
                        "Harness-compute separation",
                        "Brain/hands split",
                        "Model-sandbox separation"
                      ],
                      "category": "Agents & Autonomy",
                      "maturity": "Emerging",
                      "shortDefinition": "Separating the model-and-harness layer that decides what to do from the sandbox, tools, or systems that perform actions."
                    }
                  ],
                  "total": 3,
                  "query": {
                    "q": "harness"
                  }
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/ai-terms/{slug}": {
      "get": {
        "operationId": "getAiTerm",
        "tags": [
          "ai-terms"
        ],
        "summary": "Get one dictionary entry",
        "description": "Returns the full dictionary entry: definition, plain-English explanation, example, why it matters, related terms, and sources.",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "Entry slug as listed by `searchAiTerms`.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
              "maxLength": 120
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The dictionary entry.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AiTerm"
                },
                "example": {
                  "slug": "agent-harness",
                  "term": "Agent Harness",
                  "aliases": [
                    "Harness",
                    "Agent runtime"
                  ],
                  "category": "Agents & Autonomy",
                  "maturity": "Rising",
                  "shortDefinition": "The runtime around a model that supplies tools, state, prompts, permissions, logs, and execution loops.",
                  "plainEnglish": "The model is the brain, but the harness is the workbench, clipboard, tool belt, and supervisor.",
                  "example": "A coding harness gives an agent shell access, file editing, browser QA, progress logs, and rules for ending each session cleanly.",
                  "whyItMatters": "Many agent improvements now come from better harness design rather than only stronger models.",
                  "related": [
                    {
                      "label": "Agent Loop",
                      "slug": "agent-loop",
                      "url": "https://www.joelcsaji.com/ai-dictionary/agent-loop"
                    },
                    {
                      "label": "Long-Running Agent",
                      "slug": "long-running-agent",
                      "url": "https://www.joelcsaji.com/ai-dictionary/long-running-agent"
                    },
                    {
                      "label": "Compaction",
                      "slug": "compaction",
                      "url": "https://www.joelcsaji.com/ai-dictionary/compaction"
                    }
                  ],
                  "sources": [
                    {
                      "label": "Anthropic: Effective Harnesses",
                      "url": "https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents"
                    },
                    {
                      "label": "Anthropic: Building Effective Agents",
                      "url": "https://www.anthropic.com/engineering/building-effective-agents"
                    }
                  ],
                  "url": "https://www.joelcsaji.com/ai-dictionary/agent-harness"
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/search-index": {
      "get": {
        "operationId": "getSearchIndex",
        "tags": [
          "search"
        ],
        "summary": "Get the site search index",
        "description": "Returns the flat entry list that powers the site's command palette (⌘K): pages, articles, portfolio projects, and AI dictionary terms. The corpus is small enough to filter in memory; match against `title`, `description`, and `keywords`, plus `bodyTerms` on post entries for article-body vocabulary, and group by `kind`.",
        "responses": {
          "200": {
            "description": "The search index: pages, articles, projects, and dictionary terms.",
            "headers": {
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SearchIndex"
                },
                "example": {
                  "generatedAt": "2026-10-12T14:37:20.000Z",
                  "entries": [
                    {
                      "kind": "page",
                      "title": "Joel Saji - Software Designer & Engineer in Toronto",
                      "path": "/",
                      "description": "The personal site of Joel Saji, a Toronto software designer and engineer building fast, polished web products with React, TypeScript, and TanStack Start.",
                      "keywords": [
                        "Toronto product engineer",
                        "freelance web developer",
                        "React developer Toronto",
                        "TanStack Start developer",
                        "design-focused web development",
                        "UX-focused product engineer"
                      ]
                    },
                    {
                      "kind": "post",
                      "title": "iOS 27, iPadOS 27, macOS 27, watchOS 27 and tvOS 27",
                      "path": "/blog/ios-27-ipados-27-macos-27-watchos-27-tvos-27",
                      "description": "Every change in iOS 27, iPadOS 27, macOS 27, watchOS 27 and tvOS 27: Siri AI, the Mac redesign, new APIs, deprecations and what did not ship.",
                      "keywords": [
                        "Mobile Development",
                        "Apple",
                        "iOS",
                        "macOS",
                        "Apple Intelligence"
                      ],
                      "bodyTerms": [
                        "siri",
                        "iphone",
                        "ipad",
                        "watchos"
                      ]
                    },
                    {
                      "kind": "project",
                      "title": "Church Grid",
                      "path": "/portfolio/church-grid",
                      "description": "A church operations platform for members, events, communication, donations, and reporting.",
                      "keywords": [
                        "Next.js",
                        "React",
                        "TypeScript",
                        "Tailwind CSS",
                        "PostgreSQL",
                        "Auth.js"
                      ]
                    },
                    {
                      "kind": "term",
                      "title": "AI Agent",
                      "path": "/ai-dictionary/ai-agent",
                      "description": "A system that uses a model, tools, context, and control logic to pursue a goal and take actions.",
                      "keywords": [
                        "Agent",
                        "LLM agent"
                      ]
                    }
                  ]
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NotModified"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    }
  },
  "components": {
    "headers": {
      "Allow": {
        "description": "Methods this endpoint serves, always `GET, HEAD`.",
        "schema": {
          "type": "string",
          "example": "GET, HEAD"
        }
      },
      "ETag": {
        "description": "Strong validator derived from the response body; echo it in `If-None-Match` to revalidate.",
        "schema": {
          "type": "string",
          "example": "\"0b8f9c6e\""
        }
      },
      "RateLimit": {
        "description": "Remaining quota and seconds until reset, e.g. `\"default\";r=59;t=42`.",
        "schema": {
          "type": "string"
        }
      },
      "RateLimit-Policy": {
        "description": "The quota policy, e.g. `\"default\";q=60;w=60`.",
        "schema": {
          "type": "string"
        }
      },
      "Retry-After": {
        "description": "Seconds to wait before retrying.",
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      }
    },
    "responses": {
      "NotModified": {
        "description": "The `If-None-Match` header matched the current `ETag`; the cached copy is still fresh. The body is empty.",
        "headers": {
          "ETag": {
            "$ref": "#/components/headers/ETag"
          },
          "RateLimit": {
            "$ref": "#/components/headers/RateLimit"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimit-Policy"
          }
        }
      },
      "NotFound": {
        "description": "No item matches the identifier.",
        "headers": {
          "RateLimit": {
            "$ref": "#/components/headers/RateLimit"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimit-Policy"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "BadRequest": {
        "description": "A query parameter is invalid; the hint lists accepted values.",
        "headers": {
          "RateLimit": {
            "$ref": "#/components/headers/RateLimit"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimit-Policy"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "The client exceeded the request quota.",
        "headers": {
          "RateLimit": {
            "$ref": "#/components/headers/RateLimit"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimit-Policy"
          },
          "Retry-After": {
            "$ref": "#/components/headers/Retry-After"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "MethodNotAllowed": {
        "description": "The endpoint is read-only; `Allow` lists the supported methods.",
        "headers": {
          "Allow": {
            "$ref": "#/components/headers/Allow"
          },
          "RateLimit": {
            "$ref": "#/components/headers/RateLimit"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimit-Policy"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "InternalError": {
        "description": "The request could not be completed.",
        "headers": {
          "RateLimit": {
            "$ref": "#/components/headers/RateLimit"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimit-Policy"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "SearchIndex": {
        "type": "object",
        "required": [
          "generatedAt",
          "entries"
        ],
        "properties": {
          "generatedAt": {
            "type": "string",
            "format": "date-time",
            "description": "When this build's index was generated. Stable for a given deployment, so the response revalidates."
          },
          "entries": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SearchEntry"
            }
          }
        }
      },
      "SearchEntry": {
        "type": "object",
        "required": [
          "kind",
          "title",
          "path"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "page",
              "post",
              "project",
              "term"
            ]
          },
          "title": {
            "type": "string"
          },
          "path": {
            "type": "string",
            "description": "Site path the entry navigates to."
          },
          "description": {
            "type": "string"
          },
          "keywords": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Extra match terms: aliases, categories, keywords. Never displayed."
          },
          "bodyTerms": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "maxItems": 12,
            "description": "Post entries only: distinctive lowercase terms derived from the article body, so matching can hit vocabulary that appears in the body alone. Absent when the body yields no terms. Never displayed."
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message",
              "hint"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "not_found",
                  "invalid_parameter",
                  "rate_limited",
                  "method_not_allowed",
                  "internal_error"
                ]
              },
              "message": {
                "type": "string",
                "description": "What went wrong, in plain language."
              },
              "hint": {
                "type": "string",
                "description": "How to resolve the problem."
              }
            }
          }
        }
      },
      "Tag": {
        "type": "object",
        "required": [
          "value",
          "label"
        ],
        "properties": {
          "value": {
            "type": "string",
            "description": "URL-safe tag id."
          },
          "label": {
            "type": "string",
            "description": "Display label."
          }
        }
      },
      "PostSummary": {
        "type": "object",
        "required": [
          "slug",
          "title",
          "description",
          "author",
          "category",
          "tags",
          "publishedAt",
          "readTimeMinutes",
          "url"
        ],
        "properties": {
          "slug": {
            "type": "string",
            "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
            "maxLength": 120
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "author": {
            "type": "object",
            "required": [
              "name"
            ],
            "properties": {
              "name": {
                "type": "string",
                "description": "Author display name, as published in the RSS dc:creator."
              }
            }
          },
          "category": {
            "type": "string"
          },
          "tags": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Tag"
            }
          },
          "publishedAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "readTimeMinutes": {
            "type": "integer",
            "minimum": 1
          },
          "keywords": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "coverImage": {
            "type": "string",
            "format": "uri",
            "description": "Absolute cover image URL. Absent when the article has no cover image."
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Canonical HTML URL of the article."
          }
        }
      },
      "Post": {
        "allOf": [
          {
            "$ref": "#/components/schemas/PostSummary"
          },
          {
            "type": "object",
            "required": [
              "content"
            ],
            "properties": {
              "content": {
                "type": "string",
                "description": "Article body as Markdown."
              }
            }
          }
        ]
      },
      "CaseStudySection": {
        "type": "object",
        "required": [
          "title",
          "body"
        ],
        "properties": {
          "title": {
            "type": "string"
          },
          "body": {
            "type": "string"
          }
        }
      },
      "CaseStudy": {
        "type": "object",
        "required": [
          "sections"
        ],
        "properties": {
          "sections": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CaseStudySection"
            }
          },
          "architecture": {
            "type": "string"
          },
          "outcome": {
            "type": "string"
          },
          "results": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "highlights": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "lessonsLearned": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "Project": {
        "type": "object",
        "required": [
          "id",
          "title",
          "description",
          "technologies",
          "featured",
          "url"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
            "maxLength": 120
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "seoDescription": {
            "type": "string",
            "description": "Standalone summary for search engines; absent when the project defines none."
          },
          "technologies": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "featured": {
            "type": "boolean"
          },
          "role": {
            "type": "string"
          },
          "timeline": {
            "type": "string"
          },
          "liveUrl": {
            "type": "string",
            "format": "uri"
          },
          "sourceUrl": {
            "type": "string",
            "format": "uri"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Canonical HTML URL of the case study."
          },
          "caseStudy": {
            "$ref": "#/components/schemas/CaseStudy"
          }
        }
      },
      "NowRow": {
        "type": "object",
        "required": [
          "label",
          "value"
        ],
        "properties": {
          "label": {
            "type": "string",
            "description": "Row heading, e.g. `Now`, `Studying`, or `Base`."
          },
          "value": {
            "type": "string",
            "description": "Row text exactly as the pages render it."
          }
        }
      },
      "NowSnapshot": {
        "type": "object",
        "required": [
          "lastUpdated",
          "items",
          "url"
        ],
        "properties": {
          "lastUpdated": {
            "type": "string",
            "format": "date",
            "description": "The day the snapshot was last revised, in UTC."
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/NowRow"
            }
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Canonical HTML URL of the /now page."
          }
        }
      },
      "AiTermSummary": {
        "type": "object",
        "required": [
          "slug",
          "term",
          "aliases",
          "category",
          "maturity",
          "shortDefinition"
        ],
        "properties": {
          "slug": {
            "type": "string",
            "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
            "maxLength": 120
          },
          "term": {
            "type": "string"
          },
          "aliases": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "category": {
            "type": "string",
            "enum": [
              "Agents & Autonomy",
              "Context & Memory",
              "Tools & Protocols",
              "Retrieval & Grounding",
              "Reasoning & Inference",
              "Evaluation & Safety",
              "Models & Training",
              "AI-Native Product"
            ]
          },
          "maturity": {
            "type": "string",
            "enum": [
              "Established",
              "Rising",
              "Emerging",
              "Watch"
            ]
          },
          "shortDefinition": {
            "type": "string"
          }
        }
      },
      "AiTermRelated": {
        "type": "object",
        "required": [
          "label"
        ],
        "properties": {
          "label": {
            "type": "string",
            "description": "Display name of the related entry."
          },
          "slug": {
            "type": "string",
            "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$",
            "maxLength": 120,
            "description": "Set when `label` resolves to a dictionary entry."
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "HTML URL of the resolved entry; set with `slug`."
          }
        }
      },
      "AiTerm": {
        "allOf": [
          {
            "$ref": "#/components/schemas/AiTermSummary"
          },
          {
            "type": "object",
            "required": [
              "plainEnglish",
              "example",
              "whyItMatters",
              "related",
              "sources",
              "url"
            ],
            "properties": {
              "plainEnglish": {
                "type": "string"
              },
              "example": {
                "type": "string"
              },
              "whyItMatters": {
                "type": "string"
              },
              "related": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/AiTermRelated"
                },
                "description": "Related entries. `label` is always present; `slug` and `url` are present when the label resolves to a corpus entry, so the relation can be followed without a second lookup. A label without a `slug` is prose only, not a dictionary entry."
              },
              "sources": {
                "type": "array",
                "items": {
                  "type": "object",
                  "required": [
                    "label",
                    "url"
                  ],
                  "properties": {
                    "label": {
                      "type": "string"
                    },
                    "url": {
                      "type": "string",
                      "format": "uri"
                    }
                  }
                }
              },
              "url": {
                "type": "string",
                "format": "uri",
                "description": "HTML URL of the entry in the dictionary."
              }
            }
          }
        ]
      }
    }
  }
}