{
  "openapi": "3.1.0",
  "info": {
    "title": "Murit CMS Content API v1",
    "version": "1.0.0",
    "description": "Project creation uses General with storage selection, then review with optional settings and a final create action. Storage guides: /docs/storage (R2 recommended; S3 and GCS supported). Read-only multi-tenant Content API serving normalized posts, pre-rendered sanitized HTML, and sitemaps with zero build triggers. Cross-Origin Resource Sharing (CORS) is enabled with OPTIONS preflight on all public routes for direct client-side consumption. Operates a Thin Index Database + Cloud JSON Storage architecture: full article content and SEO metadata are stored in tenant cloud storage (sites/{siteKey}/posts/{postId}/thick_content.json) with thinned central database indexes, dynamically hydrated on read. Media storage operates on a dual-tier architecture: Free Starter Tier (200 MB included) on platform storage and Bring Your Own Storage (BYOS) with Cloudflare R2 / S3 / GCS subject to provider allowances and billing. CMS operators must deploy the registered 20260909_122610_legacy_redirects database migration before using legacy URL fields; frontend clients do not run CMS migrations.",
    "contact": {
      "name": "Murit CMS Support",
      "url": "https://www.murit.space/docs"
    }
  },
  "servers": [
    {
      "url": "https://www.murit.space",
      "description": "Murit CMS Instance"
    }
  ],
  "paths": {
    "/api/workspace": {
      "get": {
        "summary": "Authenticated workspace and platform-admin database storage usage",
        "security": [
          {
            "cmsJWT": []
          },
          {
            "cmsSession": []
          }
        ],
        "responses": {
          "200": {
            "description": "Workspace media totals aggregate all matching original-file sizes in the database, without media-record truncation. BYOS sites remain excluded from platformPoolUsage. databaseUsage is included only for platform admins. Measures this core database index. Default budget 5 GB (decimal); configurable with CMS_DATABASE_STORAGE_LIMIT_BYTES. No public API or MCP access.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "websites": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "organizations": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "platformPoolUsage": {
                      "type": "object"
                    },
                    "databaseUsage": {
                      "type": "object",
                      "required": [
                        "status",
                        "usedBytes",
                        "limitBytes",
                        "usagePercent",
                        "checkedAt"
                      ],
                      "properties": {
                        "status": {
                          "type": "string",
                          "enum": [
                            "healthy",
                            "warning",
                            "critical",
                            "urgent",
                            "full",
                            "unavailable"
                          ],
                          "description": "Thresholds: 80%, 90%, 95%, 100%. Unavailable never means zero usage."
                        },
                        "usedBytes": {
                          "type": [
                            "integer",
                            "null"
                          ]
                        },
                        "limitBytes": {
                          "type": "integer"
                        },
                        "usagePercent": {
                          "type": [
                            "number",
                            "null"
                          ]
                        },
                        "checkedAt": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required."
          }
        }
      }
    },
    "/api/workspace/migrate": {
      "post": {
        "summary": "Preview or import WordPress, Shopify, Payload CMS or JSON articles with original media URLs",
        "security": [
          {
            "cmsJWT": []
          },
          {
            "cmsSession": []
          }
        ],
        "description": "Authenticated website managers only (CMS session or JWT). Retains external featured and inline image URLs without copying files. Keep source hosting available or run npm run media:migrate to ingest files into BYOS. Automatically decodes named, decimal, and hex HTML entities into clean Unicode across titles, excerpts, Lexical AST text nodes, categories, and tags, while converting legacy Also Read links, social/video embeds, and unsupported blocks into native Lexical blocks. The same normalization runs on every post read/save, promoting supported blocks stored as plain paragraphs into editable native blocks. WordPress (REST API), Shopify (Admin GraphQL API or public Atom feed), and Payload CMS (standard Payload 3 and custom RJLS endpoints) are fully supported sources; rendered HTML is parsed as a document tree so images (captions, links), YouTube/Vimeo/MP4 video, social embeds, Also Read links, Gutenberg columns and separators become native blocks, and anything else becomes a sanitized legacyHtml block. Responses include a per-batch conversion report. Public site keys do not authorize migration.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "websiteId"
                ],
                "properties": {
                  "websiteId": {
                    "type": "integer"
                  },
                  "action": {
                    "type": "string",
                    "enum": [
                      "preview",
                      "import",
                      "resolve-links"
                    ],
                    "default": "import",
                    "description": "resolve-links links Also Read blocks in the given postIds to articles imported in later batches."
                  },
                  "source": {
                    "type": "string",
                    "enum": [
                      "wordpress",
                      "shopify-admin",
                      "shopify-feed",
                      "payload",
                      "json-url",
                      "json"
                    ],
                    "description": "Source adapter. Inferred when omitted: endpointURL containing /wp-json/ uses wordpress, other endpointURL values use json-url, otherwise posts are read."
                  },
                  "siteURL": {
                    "type": "string",
                    "description": "wordpress: site address or REST URL."
                  },
                  "page": {
                    "type": "integer",
                    "minimum": 1,
                    "description": "wordpress/payload: 1-based page."
                  },
                  "payloadSiteURL": {
                    "type": "string",
                    "description": "payload: site address or collection URL (e.g. /api/posts or /rjls-api/posts)."
                  },
                  "shopDomain": {
                    "type": "string",
                    "description": "shopify-admin: *.myshopify.com domain."
                  },
                  "accessToken": {
                    "type": "string",
                    "description": "shopify-admin: Admin API token with read_content scope. Used for the request only; never stored."
                  },
                  "cursor": {
                    "type": "string",
                    "description": "shopify-admin: pagination cursor from pagination.nextCursor."
                  },
                  "feedURL": {
                    "type": "string",
                    "description": "shopify-feed: blog address such as https://store.com/blogs/news."
                  },
                  "postIds": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    },
                    "description": "resolve-links: post IDs returned by import batches."
                  },
                  "sourceOrigin": {
                    "type": "string",
                    "format": "uri",
                    "description": "Original website origin used to resolve root-relative image paths. Defaults to destination primaryURL."
                  },
                  "endpointURL": {
                    "type": "string",
                    "format": "uri"
                  },
                  "posts": {
                    "type": "array",
                    "maxItems": 200,
                    "items": {
                      "type": "object",
                      "additionalProperties": true
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Preview or import result; imports may contain per-article errors. Includes a unique list of retained featured/inline URLs.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "mediaPreservation": {
                      "type": "object",
                      "properties": {
                        "strategy": {
                          "const": "preserve-urls"
                        },
                        "sourceOrigin": {
                          "type": "string"
                        },
                        "urls": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "format": "uri"
                          }
                        },
                        "hostingVerified": {
                          "const": false
                        },
                        "notice": {
                          "type": "string"
                        }
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "description": "wordpress: { page, totalPages, total }; payload: { page, totalPages, total }; shopify-admin: { nextCursor }.",
                      "additionalProperties": true
                    },
                    "skippedCount": {
                      "type": "integer",
                      "description": "Items without a title that were not imported."
                    },
                    "postIds": {
                      "type": "array",
                      "items": {
                        "type": "integer"
                      },
                      "description": "import: saved post IDs."
                    },
                    "report": {
                      "type": "object",
                      "properties": {
                        "articles": {
                          "type": "integer"
                        },
                        "cleanArticles": {
                          "type": "integer"
                        },
                        "elements": {
                          "type": "object",
                          "properties": {
                            "images": {
                              "type": "integer"
                            },
                            "videos": {
                              "type": "integer"
                            },
                            "embeds": {
                              "type": "integer"
                            },
                            "callouts": {
                              "type": "integer"
                            },
                            "buttons": {
                              "type": "integer"
                            },
                            "faqs": {
                              "type": "integer"
                            },
                            "galleries": {
                              "type": "integer"
                            },
                            "tables": {
                              "type": "integer"
                            },
                            "socialEmbeds": {
                              "type": "integer"
                            },
                            "alsoRead": {
                              "type": "integer"
                            },
                            "columns": {
                              "type": "integer"
                            },
                            "horizontalRules": {
                              "type": "integer"
                            },
                            "legacyHtml": {
                              "type": "integer"
                            }
                          }
                        },
                        "preserved": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "integer"
                          },
                          "description": "Legacy HTML labels (Table, Gallery, Buttons, ...) with counts."
                        },
                        "needsReview": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "title": {
                                "type": "string"
                              },
                              "slug": {
                                "type": "string"
                              },
                              "postId": {
                                "type": "integer"
                              },
                              "preserved": {
                                "type": "array",
                                "items": {
                                  "type": "string"
                                }
                              },
                              "warnings": {
                                "type": "array",
                                "items": {
                                  "type": "string"
                                }
                              }
                            }
                          }
                        }
                      }
                    },
                    "linked": {
                      "type": "integer",
                      "description": "resolve-links: Also Read blocks linked."
                    },
                    "updatedPosts": {
                      "type": "integer",
                      "description": "resolve-links: posts saved."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid action, batch, image URL or unpopulated foreign media ID."
          },
          "401": {
            "description": "Authentication required."
          },
          "403": {
            "description": "Website manager access required."
          },
          "429": {
            "description": "Source rate limit reached (Shopify); resume after a short wait."
          },
          "502": {
            "description": "Remote source failed or redirected to a non-public address."
          },
          "504": {
            "description": "Remote source timed out."
          }
        }
      }
    },
    "/api/workspace/migrate-media": {
      "get": {
        "summary": "Audit legacy media references and ingestion status for a website",
        "security": [
          {
            "cmsJWT": []
          },
          {
            "cmsSession": []
          }
        ],
        "description": "Authenticated website managers only. Scans all articles belonging to the specified website for legacy image references. Returns total media detected, already uploaded count, pending count, storage provider label, and a sample array.",
        "parameters": [
          {
            "name": "websiteId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "Target website ID"
          },
          {
            "name": "sourceOrigin",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uri"
            },
            "description": "Original site origin to resolve relative media paths"
          }
        ],
        "responses": {
          "200": {
            "description": "Legacy media audit results",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "websiteId": {
                      "type": "integer"
                    },
                    "websiteName": {
                      "type": "string"
                    },
                    "total": {
                      "type": "integer"
                    },
                    "alreadyUploaded": {
                      "type": "integer"
                    },
                    "pending": {
                      "type": "integer"
                    },
                    "storageProvider": {
                      "type": "string"
                    },
                    "storageProviderLabel": {
                      "type": "string"
                    },
                    "sample": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "items": {
                      "type": "array",
                      "description": "Complete list of discovered legacy media assets with filename, storage paths, source/storage URLs, status, and associated articles",
                      "items": {
                        "type": "object",
                        "properties": {
                          "filename": {
                            "type": "string"
                          },
                          "relativePath": {
                            "type": "string"
                          },
                          "sourceURL": {
                            "type": "string"
                          },
                          "storageURL": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "uploaded",
                              "pending"
                            ]
                          },
                          "mediaId": {
                            "type": "integer"
                          },
                          "postTitles": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "A valid websiteId is required."
          },
          "401": {
            "description": "Authentication required."
          },
          "403": {
            "description": "Website manager access required."
          }
        }
      },
      "post": {
        "summary": "Trigger incremental batch media ingestion or audit",
        "security": [
          {
            "cmsJWT": []
          },
          {
            "cmsSession": []
          }
        ],
        "description": "Authenticated website managers only. Downloads legacy media files in safe batches (preventing gateway timeouts) and ingests them into connected storage (Murit Cloud, Cloudflare R2, AWS S3, GCS). Automatically updates media records, post relationships, and Lexical AST content.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "websiteId"
                ],
                "properties": {
                  "websiteId": {
                    "type": "integer"
                  },
                  "action": {
                    "type": "string",
                    "enum": [
                      "batch",
                      "audit"
                    ],
                    "default": "batch"
                  },
                  "batchSize": {
                    "type": "integer",
                    "default": 5,
                    "minimum": 1,
                    "maximum": 20
                  },
                  "concurrency": {
                    "type": "integer",
                    "default": 3,
                    "minimum": 1,
                    "maximum": 5
                  },
                  "rewriteMode": {
                    "type": "string",
                    "enum": [
                      "relative",
                      "cdn"
                    ],
                    "default": "relative"
                  },
                  "skipUrls": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "sourceOrigin": {
                    "type": "string",
                    "format": "uri"
                  },
                  "force": {
                    "type": "boolean",
                    "default": false
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Batch ingestion or audit result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "done": {
                      "type": "boolean"
                    },
                    "totalMedia": {
                      "type": "integer"
                    },
                    "alreadyUploaded": {
                      "type": "integer"
                    },
                    "pendingCount": {
                      "type": "integer"
                    },
                    "remaining": {
                      "type": "integer"
                    },
                    "processed": {
                      "type": "integer"
                    },
                    "uploadedCount": {
                      "type": "integer"
                    },
                    "failedCount": {
                      "type": "integer"
                    },
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "errors": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "storageProvider": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid action or parameters."
          },
          "401": {
            "description": "Authentication required."
          },
          "403": {
            "description": "Website manager access required."
          }
        }
      }
    },
    "/api/workspace/social-preview": {
      "get": {
        "summary": "Fetch server-side social embed metadata and link preview",
        "security": [
          {
            "cmsJWT": []
          },
          {
            "cmsSession": []
          }
        ],
        "description": "Authenticated CMS users only. Fetches social post details (author, text, image, postUrl) server-side to prevent client-side browser tracking prevention blocks (Edge, Brave Shields).",
        "parameters": [
          {
            "name": "url",
            "in": "query",
            "required": true,
            "description": "Target social post URL or embed URL",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "platform",
            "in": "query",
            "required": false,
            "description": "Platform identifier (x, instagram, linkedin, facebook)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Social embed metadata preview",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "platform": {
                          "type": "string"
                        },
                        "author": {
                          "type": "string"
                        },
                        "authorHandle": {
                          "type": "string"
                        },
                        "text": {
                          "type": "string"
                        },
                        "imageUrl": {
                          "type": "string"
                        },
                        "postUrl": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing url or unsupported social embed URL."
          },
          "401": {
            "description": "Authentication required."
          }
        }
      }
    },
    "/api/workspace/resolve-image": {
      "get": {
        "summary": "Resolve a link to a direct image URL",
        "security": [
          {
            "cmsJWT": []
          },
          {
            "cmsSession": []
          }
        ],
        "description": "Authenticated CMS users only. Resolves a pasted link to a direct image URL: direct image links pass through, web page links (e.g. Unsplash photo pages) are resolved from their OpenGraph preview image. Rejects non-http(s) URLs and private/internal hosts. Images are referenced by URL and never copied to storage.",
        "parameters": [
          {
            "name": "url",
            "in": "query",
            "required": true,
            "description": "Direct image URL or a web page URL to resolve",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Resolved image URL",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "imageUrl": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing/invalid url or disallowed host."
          },
          "401": {
            "description": "Authentication required."
          },
          "422": {
            "description": "No image found at the URL."
          },
          "502": {
            "description": "Remote source failed or returned an error."
          }
        }
      }
    },
    "/api/workspace/websites/{id}/delete-request": {
      "post": {
        "summary": "Request project deletion with a 48-hour grace period",
        "security": [
          {
            "cmsJWT": []
          },
          {
            "cmsSession": []
          }
        ],
        "description": "Authenticated website managers only. Requests deletion for the website, immediately pauses public Content API delivery, and schedules permanent cascade purge in 48 hours. Requires typing project name or DELETE to confirm.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Website ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "confirmationText"
                ],
                "properties": {
                  "confirmationText": {
                    "type": "string",
                    "description": "Exact project name or the keyword DELETE in capital letters"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Project deletion scheduled and 48-hour grace period active.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "website": {
                      "type": "object"
                    },
                    "scheduledForDeletionAt": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid confirmation text or website ID."
          },
          "401": {
            "description": "Authentication required."
          },
          "403": {
            "description": "Permission denied."
          },
          "409": {
            "description": "Project is already pending deletion."
          }
        }
      }
    },
    "/api/workspace/websites/{id}/restore": {
      "post": {
        "summary": "Cancel deletion request and restore project to active",
        "security": [
          {
            "cmsJWT": []
          },
          {
            "cmsSession": []
          }
        ],
        "description": "Authenticated website managers only. Cancels the scheduled purge within the 48-hour grace period, restores project status to active, and resumes public Content API delivery. Once the grace period has elapsed the request is rejected with HTTP 410 and the project is permanently purged instead.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Website ID",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Project restored to active status.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "website": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Project is not pending deletion or invalid ID."
          },
          "401": {
            "description": "Authentication required."
          },
          "403": {
            "description": "Permission denied."
          },
          "410": {
            "description": "The 48-hour deletion grace period has expired. The project can no longer be restored and is being permanently purged."
          }
        }
      }
    },
    "/api/content/v1/sites/{siteKey}": {
      "get": {
        "summary": "Get site metadata",
        "description": "Returns public configuration and metadata for a registered website.",
        "operationId": "getSiteMetadata",
        "parameters": [
          {
            "name": "siteKey",
            "in": "path",
            "required": true,
            "description": "The immutable public site key (e.g. site_...)",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Site metadata returned successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "name": {
                          "type": "string"
                        },
                        "primaryURL": {
                          "type": "string"
                        },
                        "defaultLocale": {
                          "type": "string"
                        },
                        "siteKey": {
                          "type": "string"
                        },
                        "blogBasePath": {
                          "type": "string"
                        },
                        "timezone": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/content/v1/sites/{siteKey}/posts": {
      "get": {
        "summary": "List published posts",
        "description": "Returns a paginated list of published post summaries for the given website.",
        "operationId": "listPublishedPosts",
        "parameters": [
          {
            "name": "siteKey",
            "in": "path",
            "required": true,
            "description": "Public site key",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Number of posts to return (1-50, default 20)",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 20
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Opaque cursor for pagination from previous response",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "description": "Filter posts by category slug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "tag",
            "in": "query",
            "required": false,
            "description": "Filter posts by tag slug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "description": "Set to \"astro\" to return { posts: AstroPost[] } instead of the default paginated DTO envelope.",
            "schema": {
              "type": "string",
              "enum": [
                "default",
                "astro",
                "legacy"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Paginated list of posts, or an AstroPost envelope when format=astro/legacy",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/PostSummary"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "limit": {
                          "type": "integer"
                        },
                        "nextCursor": {
                          "type": [
                            "string",
                            "null"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/content/v1/sites/{siteKey}/legacy-posts": {
      "get": {
        "summary": "List published posts in the Astro/legacy contract",
        "description": "Returns { posts: AstroPost[] }, the envelope expected by Astro SSG and legacy frontends.",
        "operationId": "listLegacyPosts",
        "parameters": [
          {
            "name": "siteKey",
            "in": "path",
            "required": true,
            "description": "Public site key",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "description": "Filter posts by category slug",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "tag",
            "in": "query",
            "required": false,
            "description": "Filter posts by tag slug",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "List of posts in Astro-compatible contract",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "posts": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AstroPost"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/content/v1/sites/{siteKey}/posts/{slug}": {
      "get": {
        "summary": "Get single published post",
        "description": "Retrieves full article details including sanitized lexical HTML, categories, and SEO metadata.",
        "operationId": "getPostBySlug",
        "parameters": [
          {
            "name": "siteKey",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "description": "The post slug, or a legacy/redirect path (absolute URL, root-relative path, or multi-segment path) to resolve via an automatic 308 redirect.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "description": "Set to \"astro\" to return { post: AstroPost } instead of { data: PostDetail }.",
            "schema": {
              "type": "string",
              "enum": [
                "default",
                "astro",
                "legacy"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Full post details",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/PostDetail"
                    }
                  }
                }
              }
            }
          },
          "308": {
            "description": "Permanent Redirect returned when the requested path matches a published post's legacyUrl or redirectUrls entry. The request is normalized (origin, leading/trailing slashes, query, and hash ignored), so absolute permalinks, root-relative paths, and multi-segment paths all resolve. The Location header is the canonical public site URL.",
            "headers": {
              "Location": {
                "description": "Canonical public site URL of the post",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "redirect": {
                      "type": "object",
                      "properties": {
                        "destination": {
                          "type": "string",
                          "description": "Bare canonical slug to re-query the API with."
                        },
                        "location": {
                          "type": "string",
                          "description": "Canonical public site URL."
                        },
                        "permanent": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/rjls-api/posts": {
      "get": {
        "summary": "Legacy compatibility alias for /legacy-posts",
        "description": "Backward-compatibility alias for the pre-migration RJLS frontend. Requires an explicit registered siteKey and returns only that website’s published posts.",
        "operationId": "getLegacyPostsAlias",
        "parameters": [
          {
            "name": "siteKey",
            "in": "query",
            "required": true,
            "description": "The registered public site key; there is no implicit fallback tenant.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Astro/legacy posts envelope",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "posts": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/AstroPost"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing siteKey query parameter"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/api/content/v1/sites/{siteKey}/sitemap": {
      "get": {
        "summary": "Retrieve sitemap index",
        "description": "Returns published article URLs and last-modified ISO timestamps for SEO sitemap generation.",
        "operationId": "getSitemapIndex",
        "parameters": [
          {
            "name": "siteKey",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Sitemap list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/SitemapItem"
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "limit": {
                          "type": "integer"
                        },
                        "nextCursor": {
                          "type": [
                            "string",
                            "null"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/content/v1/sites/{siteKey}/preview": {
      "get": {
        "summary": "Redeem private draft preview",
        "description": "Access unpublished saved drafts using an ephemeral 5-minute cryptographic token.",
        "operationId": "previewDraft",
        "parameters": [
          {
            "name": "siteKey",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "token",
            "in": "query",
            "required": true,
            "description": "Cryptographic signed preview token",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Draft post content",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/PostDetail"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Invalid or expired preview token"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "cmsJWT": {
        "type": "apiKey",
        "in": "header",
        "name": "Authorization",
        "description": "JWT <token> from CMS login."
      },
      "cmsSession": {
        "type": "apiKey",
        "in": "cookie",
        "name": "payload-token",
        "description": "CMS session; cookie-authenticated writes require a valid same-origin request."
      }
    },
    "responses": {
      "NotFound": {
        "description": "Website, post, or resource not found",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "error": {
                  "type": "object",
                  "properties": {
                    "code": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "schemas": {
      "Media": {
        "type": "object",
        "properties": {
          "url": {
            "type": [
              "string",
              "null"
            ],
            "description": "May reference original external hosting. Preserve this URL; CMS migration does not copy or verify the source file."
          },
          "caption": {
            "type": [
              "string",
              "null"
            ]
          },
          "alt": {
            "type": [
              "string",
              "null"
            ]
          },
          "width": {
            "type": [
              "integer",
              "null"
            ]
          },
          "height": {
            "type": [
              "integer",
              "null"
            ]
          }
        }
      },
      "Taxonomy": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          }
        }
      },
      "PostSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "excerpt": {
            "type": "string"
          },
          "modifiedAt": {
            "type": "string",
            "format": "date-time"
          },
          "featuredMedia": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Media"
              },
              {
                "type": "null"
              }
            ]
          },
          "title": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "publishedAt": {
            "type": "string",
            "format": "date-time"
          },
          "categories": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Taxonomy"
            }
          },
          "tags": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Taxonomy"
            }
          },
          "seo": {
            "type": "object",
            "properties": {
              "title": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "description": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "canonicalURL": {
                "type": "string"
              },
              "keywords": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "socialImage": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/Media"
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            }
          }
        }
      },
      "PostDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/PostSummary"
          },
          {
            "type": "object",
            "properties": {
              "contentHtml": {
                "type": "string",
                "description": "Sanitized, pre-rendered lexical HTML ready for direct injection. Supports video embeds (YouTube, Vimeo), video players (uploaded and streaming MP4/WebM), Social Embeds (X/Twitter, Instagram, LinkedIn, Facebook with responsive 50% compact sizing), Also Read cards, checklists, code blocks (<pre><code class=\"language-{lang}\"> with HTML-escaped code), inline images (optional <figcaption> and wrapping link), and responsive column grids (.content-columns--100, .content-columns--50-50, .content-columns--30-70, .content-columns--70-30, .content-columns--33-33-33, .content-columns--25-50-25) with optional custom fractional grid inline styles. Captioned YouTube/Vimeo embeds are wrapped in figure.content-embed-figure. Imported markup with no native block (tables, galleries, buttons) is delivered as allowlist-sanitized HTML inside div.content-legacy-html. Links keep SEO rel tokens nofollow, sponsored and ugc (set in the editor or imported), with noopener noreferrer appended for target=\"_blank\" links."
              }
            }
          }
        ]
      },
      "SitemapItem": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string"
          },
          "lastModified": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "AstroPost": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "date": {
            "type": "string",
            "format": "date-time"
          },
          "excerpt": {
            "type": "string"
          },
          "image": {
            "type": [
              "string",
              "null"
            ]
          },
          "imageAlt": {
            "type": "string"
          },
          "imageWidth": {
            "type": [
              "number",
              "null"
            ]
          },
          "imageHeight": {
            "type": [
              "number",
              "null"
            ]
          },
          "seoImage": {
            "type": [
              "string",
              "null"
            ]
          },
          "seoImageWidth": {
            "type": [
              "number",
              "null"
            ]
          },
          "seoImageHeight": {
            "type": [
              "number",
              "null"
            ]
          },
          "categories": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "seoKeywords": {
            "type": [
              "string",
              "null"
            ]
          },
          "seoTitle": {
            "type": "string"
          },
          "seoDescription": {
            "type": "string"
          },
          "contentHtml": {
            "type": "string",
            "description": "Sanitized, pre-rendered lexical HTML ready for direct injection. Video embeds, video players, Also Read cards, checklists, code blocks (<pre><code class=\"language-{lang}\">), inline images, and responsive column grids are pre-rendered. Links keep rel nofollow/sponsored/ugc."
          },
          "contentMarkdown": {
            "type": "string"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "lastUpdatedAt": {
            "type": [
              "string",
              "null"
            ]
          },
          "redirectUrls": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "legacyUrl": {
            "type": "string"
          }
        }
      }
    }
  }
}