openapi: 3.1.0
info:
  title: PiratePage API
  version: 1.0.0
  description: |
    Programmatic access to PiratePage — AI-powered landing page generation.

    Use this API to create projects, fill wizard answers, trigger AI generation, edit
    sections, manage variations, and export the finished page as Markdown. All endpoints
    require an API key issued from your PiratePage account settings (Agency plan).

    **Base URL:** `https://piratepage.cc/api/v1`

    **Authentication:** Pass your API key in the `Authorization` header as a Bearer token:
    ```
    Authorization: Bearer pp_live_xxxxxxxxxxxxxxxx
    ```

    **Error shape:** Every non-2xx response returns `{ "error": { "code": "...", "message": "..." } }`.

    **Plan limits:** HTTP 402 is returned when an action exceeds your plan. The error code
    will be `PLAN_LIMIT`.

servers:
  - url: https://piratepage.cc/api/v1
    description: Production

security:
  - bearerAuth: []

tags:
  - name: Projects
    description: Create and manage projects (one per product/brand).
  - name: Knowledge Base
    description: Per-project knowledge base — markdown that informs every AI generation.
  - name: Pages
    description: Landing pages inside a project.
  - name: Wizard
    description: The 9-question positioning wizard that feeds AI generation.
  - name: Generation
    description: AI page and section generation.
  - name: Sections
    description: Individual section CRUD and reordering within a page.
  - name: Variations
    description: AI-generated alternative copy variations for a section.

paths:

  # ──────────────────────────────────────────────────────────
  # Projects
  # ──────────────────────────────────────────────────────────

  /projects:
    get:
      operationId: listProjects
      summary: List all projects
      tags: [Projects]
      responses:
        "200":
          description: Array of projects ordered by last updated descending.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/ProjectSummary"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "500":
          $ref: "#/components/responses/InternalError"

    post:
      operationId: createProject
      summary: Create a project
      description: |
        Creates a new project and automatically creates a "Homepage" page inside it.
        The `homepageId` in the response is the ID of that auto-created page.
      tags: [Projects]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateProjectRequest"
      responses:
        "201":
          description: Project created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectCreated"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PlanLimit"
        "500":
          $ref: "#/components/responses/InternalError"

  /projects/{projectId}:
    parameters:
      - $ref: "#/components/parameters/projectId"

    get:
      operationId: getProject
      summary: Get a project
      tags: [Projects]
      responses:
        "200":
          description: Project detail including knowledge base.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectDetail"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

    patch:
      operationId: updateProject
      summary: Update a project
      description: |
        Partial update — only fields present in the body are changed.
        Accepted fields: `name`, `description`, `url`, `language`, `knowledgeBase`.
      tags: [Projects]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateProjectRequest"
      responses:
        "200":
          description: Updated project detail.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProjectDetail"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

    delete:
      operationId: deleteProject
      summary: Delete a project
      description: Permanently deletes the project and all its pages.
      tags: [Projects]
      responses:
        "200":
          description: Deleted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

  # ──────────────────────────────────────────────────────────
  # Knowledge Base
  # ──────────────────────────────────────────────────────────

  /projects/{projectId}/knowledge:
    parameters:
      - $ref: "#/components/parameters/projectId"

    get:
      operationId: getKnowledgeBase
      summary: Get project knowledge base
      description: Returns the raw Markdown knowledge base for the project.
      tags: [Knowledge Base]
      responses:
        "200":
          description: Knowledge base content.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KnowledgeBaseResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

    put:
      operationId: updateKnowledgeBase
      summary: Replace project knowledge base
      description: Fully replaces the knowledge base with the provided Markdown string (max 50 KB).
      tags: [Knowledge Base]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateKnowledgeBaseRequest"
      responses:
        "200":
          description: Updated knowledge base.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/KnowledgeBaseResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

  /projects/{projectId}/knowledge/extract:
    parameters:
      - $ref: "#/components/parameters/projectId"

    post:
      operationId: extractKnowledge
      summary: AI-extract knowledge base from URL or text
      description: |
        Uses AI to extract structured product knowledge from a URL, raw text, or both,
        and returns it as a Markdown string ready to be saved via `PUT /knowledge`.

        If the URL cannot be scraped and no `context` is provided, returns
        `{ "status": "needs_manual_input", "reason": "scrape_failed" }`.
      tags: [Knowledge Base]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ExtractKnowledgeRequest"
      responses:
        "200":
          description: |
            Extraction result. On success: `{ "markdown": "..." }`.
            On scrape failure with no text fallback: `{ "status": "needs_manual_input", "reason": "scrape_failed" | "insufficient_content" }`.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/ExtractKnowledgeSuccess"
                  - $ref: "#/components/schemas/NeedsManualInput"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

  # ──────────────────────────────────────────────────────────
  # Pages
  # ──────────────────────────────────────────────────────────

  /projects/{projectId}/pages:
    parameters:
      - $ref: "#/components/parameters/projectId"

    get:
      operationId: listPages
      summary: List pages in a project
      description: Returns all pages ordered by `order` ascending. Includes `shareUrl` when a share token exists.
      tags: [Pages]
      responses:
        "200":
          description: Array of page summaries.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/PageSummary"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

    post:
      operationId: createPage
      summary: Create a page
      description: Creates a new page inside the project. A unique slug is auto-generated from the name.
      tags: [Pages]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreatePageRequest"
      responses:
        "201":
          description: Page created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PageCreated"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PlanLimit"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

  /projects/{projectId}/pages/reorder:
    parameters:
      - $ref: "#/components/parameters/projectId"

    put:
      operationId: reorderPages
      summary: Batch reorder pages
      description: Updates `parentId`, `order`, and `isPlaced` for multiple pages in one transaction (max 200).
      tags: [Pages]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ReorderPagesRequest"
      responses:
        "200":
          description: Reorder applied.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

  /projects/{projectId}/pages/{pageId}:
    parameters:
      - $ref: "#/components/parameters/projectId"
      - $ref: "#/components/parameters/pageId"

    get:
      operationId: getPage
      summary: Get a page
      description: Returns full page detail including sections, wizard answers, generation choices, and section variations.
      tags: [Pages]
      responses:
        "200":
          description: Page detail.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PageDetail"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

    patch:
      operationId: updatePage
      summary: Update a page
      description: |
        Updates mutable page fields. Accepted fields: `name`, `promptVersion`.
        The slug is not changed when the name changes.
      tags: [Pages]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdatePageRequest"
      responses:
        "200":
          description: Updated page.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PageUpdated"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

    delete:
      operationId: deletePage
      summary: Delete a page
      tags: [Pages]
      responses:
        "200":
          description: Deleted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

  /projects/{projectId}/pages/{pageId}/duplicate:
    parameters:
      - $ref: "#/components/parameters/projectId"
      - $ref: "#/components/parameters/pageId"

    post:
      operationId: duplicatePage
      summary: Duplicate a page
      description: Creates a copy of the page including wizard answers, sections, and generation choices. Variations are not copied.
      tags: [Pages]
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DuplicatePageRequest"
      responses:
        "201":
          description: Duplicate page created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PageCreated"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

  /projects/{projectId}/pages/{pageId}/share:
    parameters:
      - $ref: "#/components/parameters/projectId"
      - $ref: "#/components/parameters/pageId"

    post:
      operationId: getOrCreateShareLink
      summary: Get or create a share link
      description: Returns the page's public share URL, creating a token if one does not yet exist.
      tags: [Pages]
      responses:
        "200":
          description: Share link.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ShareLinkResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

  /projects/{projectId}/pages/{pageId}/export:
    parameters:
      - $ref: "#/components/parameters/projectId"
      - $ref: "#/components/parameters/pageId"

    get:
      operationId: exportPage
      summary: Export page as Markdown
      description: |
        Exports the generated page as Markdown. Two formats are available via the `format`
        query parameter: `flat` (default — clean structure) and `ascii` (visual hierarchy
        with ASCII-art dividers).

        Set `Accept: text/markdown` to receive the raw Markdown string instead of JSON.
      tags: [Pages]
      parameters:
        - name: format
          in: query
          required: false
          schema:
            type: string
            enum: [flat, ascii]
            default: flat
          description: Markdown format.
      responses:
        "200":
          description: |
            Markdown export. Returns JSON by default; returns raw Markdown when
            `Accept: text/markdown` or `Accept: text/plain` is sent.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ExportPageResponse"
            text/markdown:
              schema:
                type: string
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

  # ──────────────────────────────────────────────────────────
  # Wizard
  # ──────────────────────────────────────────────────────────

  /projects/{projectId}/pages/{pageId}/wizard:
    parameters:
      - $ref: "#/components/parameters/projectId"
      - $ref: "#/components/parameters/pageId"

    put:
      operationId: saveWizardAnswers
      summary: Save wizard answers
      description: |
        Saves the positioning wizard answers for the page. Call this before triggering
        generation. You can submit partial answers and update incrementally.
        Optionally pass `language` (BCP 47 code, e.g. `"en"`, `"nl"`) to set the
        output language for AI generation.
      tags: [Wizard]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SaveWizardAnswersRequest"
      responses:
        "200":
          description: Answers saved.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SaveWizardAnswersResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

  /projects/{projectId}/pages/{pageId}/wizard/extract:
    parameters:
      - $ref: "#/components/parameters/projectId"
      - $ref: "#/components/parameters/pageId"

    post:
      operationId: extractWizardAnswers
      summary: AI-extract wizard answers from URL or text
      description: |
        Uses AI to pre-fill wizard answers from a product URL, raw description text,
        or both. The returned `answers` object can be passed directly to
        `PUT /wizard` to save them.

        If the URL cannot be scraped and no `context` is provided, returns
        `{ "status": "needs_manual_input", "reason": "scrape_failed" }`.
      tags: [Wizard]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ExtractWizardAnswersRequest"
      responses:
        "200":
          description: |
            Extraction result. On success: `{ "answers": { ... } }`.
            On failure: `{ "status": "needs_manual_input", "reason": "scrape_failed" }`.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/ExtractWizardAnswersSuccess"
                  - $ref: "#/components/schemas/NeedsManualInput"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

  # ──────────────────────────────────────────────────────────
  # Generation
  # ──────────────────────────────────────────────────────────

  /projects/{projectId}/pages/{pageId}/generate:
    parameters:
      - $ref: "#/components/parameters/projectId"
      - $ref: "#/components/parameters/pageId"

    post:
      operationId: generatePage
      summary: Generate (or regenerate) a page
      description: |
        Triggers AI generation of the full landing page from the saved wizard answers.
        Requires wizard answers to have been saved first via `PUT /wizard`.

        On first generation the page's wizard answers are used directly.
        On regeneration (page already has sections), you can optionally provide a
        `feedback` string (max 1 000 chars) to guide the revised output.

        Returns the generated sections and a share URL. Generation typically takes
        10–30 seconds.
      tags: [Generation]
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/GeneratePageRequest"
      responses:
        "200":
          description: Page generated.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GeneratePageResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PlanLimit"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

  # ──────────────────────────────────────────────────────────
  # Sections
  # ──────────────────────────────────────────────────────────

  /projects/{projectId}/pages/{pageId}/sections:
    parameters:
      - $ref: "#/components/parameters/projectId"
      - $ref: "#/components/parameters/pageId"

    put:
      operationId: replaceSections
      summary: Replace all sections
      description: |
        Atomically replaces the entire sections array. Use this for bulk reordering or
        wholesale edits. Max 50 sections, max 500 KB total payload.
      tags: [Sections]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ReplaceSectionsRequest"
      responses:
        "200":
          description: Sections replaced.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ReplaceSectionsResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PlanLimit"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

  /projects/{projectId}/pages/{pageId}/sections/add:
    parameters:
      - $ref: "#/components/parameters/projectId"
      - $ref: "#/components/parameters/pageId"

    post:
      operationId: addSection
      summary: Add a section
      description: |
        Inserts a new section into the page. Provide `type` and optionally pre-populated
        `content`. Use `insertAt` (0-based index) to place the section at a specific
        position; omit to append at the end.
      tags: [Sections]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AddSectionRequest"
      responses:
        "201":
          description: Section added.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Section"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PlanLimit"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

  /projects/{projectId}/pages/{pageId}/sections/{sectionId}:
    parameters:
      - $ref: "#/components/parameters/projectId"
      - $ref: "#/components/parameters/pageId"
      - $ref: "#/components/parameters/sectionId"

    patch:
      operationId: updateSection
      summary: Update a section's content
      description: Replaces the `content` object for a single section. Sets `isEdited: true` on the section.
      tags: [Sections]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateSectionRequest"
      responses:
        "200":
          description: Updated section.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Section"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PlanLimit"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

    delete:
      operationId: deleteSection
      summary: Delete a section
      description: Removes the section and renumbers remaining sections. Also clears any stored variations for this section.
      tags: [Sections]
      responses:
        "200":
          description: Deleted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

  /projects/{projectId}/pages/{pageId}/sections/{sectionId}/move:
    parameters:
      - $ref: "#/components/parameters/projectId"
      - $ref: "#/components/parameters/pageId"
      - $ref: "#/components/parameters/sectionId"

    post:
      operationId: moveSection
      summary: Move a section up or down
      description: Swaps the section with its neighbour and renumbers all sections.
      tags: [Sections]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MoveSectionRequest"
      responses:
        "200":
          description: Sections after move.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MoveSectionResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

  /projects/{projectId}/pages/{pageId}/sections/{sectionId}/duplicate:
    parameters:
      - $ref: "#/components/parameters/projectId"
      - $ref: "#/components/parameters/pageId"
      - $ref: "#/components/parameters/sectionId"

    post:
      operationId: duplicateSection
      summary: Duplicate a section
      description: Creates a deep copy of the section and appends it at the end.
      tags: [Sections]
      responses:
        "201":
          description: Duplicate section.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Section"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

  /projects/{projectId}/pages/{pageId}/sections/{sectionId}/reprompt:
    parameters:
      - $ref: "#/components/parameters/projectId"
      - $ref: "#/components/parameters/pageId"
      - $ref: "#/components/parameters/sectionId"

    post:
      operationId: repromptSection
      summary: Reprompt a section with an instruction
      description: |
        Re-generates the section content using a freeform instruction (e.g. "make this
        more conversational" or "emphasise the pricing"). The current content is saved as
        an "Original" variation before being replaced. The new content is also saved as a
        variation so you can compare or roll back.
      tags: [Generation]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/RepromptSectionRequest"
      responses:
        "200":
          description: Reprompted content and updated variations list.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RepromptSectionResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PlanLimit"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

  # ──────────────────────────────────────────────────────────
  # Variations
  # ──────────────────────────────────────────────────────────

  /projects/{projectId}/pages/{pageId}/sections/{sectionId}/variations:
    parameters:
      - $ref: "#/components/parameters/projectId"
      - $ref: "#/components/parameters/pageId"
      - $ref: "#/components/parameters/sectionId"

    get:
      operationId: listVariations
      summary: List variations for a section
      tags: [Variations]
      responses:
        "200":
          description: Array of section variations.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ListVariationsResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

    delete:
      operationId: clearVariations
      summary: Delete all variations for a section
      tags: [Variations]
      responses:
        "200":
          description: All variations deleted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

  /projects/{projectId}/pages/{pageId}/sections/{sectionId}/variations/generate:
    parameters:
      - $ref: "#/components/parameters/projectId"
      - $ref: "#/components/parameters/pageId"
      - $ref: "#/components/parameters/sectionId"

    post:
      operationId: generateVariations
      summary: Generate 5 tone variations for a section
      description: |
        Generates five copy variations — one per tone: `punchy`, `conversational`,
        `benefit-focused`, `problem-aware`, and `bold-confident`. All five are generated
        in parallel. The original content is also saved as a baseline variation if not
        already present. Returns all variations (original + new).
      tags: [Variations]
      responses:
        "200":
          description: Generated variations.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GenerateVariationsResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "402":
          $ref: "#/components/responses/PlanLimit"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

  /projects/{projectId}/pages/{pageId}/sections/{sectionId}/variations/{variationId}:
    parameters:
      - $ref: "#/components/parameters/projectId"
      - $ref: "#/components/parameters/pageId"
      - $ref: "#/components/parameters/sectionId"
      - $ref: "#/components/parameters/variationId"

    put:
      operationId: applyOrManageVariation
      summary: Select, discard, or restore a variation
      description: |
        Three actions:
        - `select` — applies the variation's content to the live section.
        - `discard` — marks the variation as discarded (hidden in UI, not deleted).
        - `restore` — un-discards a variation.
      tags: [Variations]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ManageVariationRequest"
      responses:
        "200":
          description: Action applied.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ManageVariationResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

    delete:
      operationId: deleteVariation
      summary: Permanently delete a variation
      tags: [Variations]
      responses:
        "200":
          description: Deleted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/InternalError"

# ──────────────────────────────────────────────────────────────────────────────
# Components
# ──────────────────────────────────────────────────────────────────────────────

components:

  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: PiratePage API key. Issue from your account settings (Agency plan required).

  parameters:
    projectId:
      name: projectId
      in: path
      required: true
      schema:
        type: string
      description: Unique project identifier (cuid).

    pageId:
      name: pageId
      in: path
      required: true
      schema:
        type: string
      description: Unique page identifier (cuid).

    sectionId:
      name: sectionId
      in: path
      required: true
      schema:
        type: string
      description: Section identifier (e.g. `section-hero-ab12`).

    variationId:
      name: variationId
      in: path
      required: true
      schema:
        type: string
      description: Variation identifier (e.g. `var-1714000000000-0`).

  responses:
    BadRequest:
      description: Validation error.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"

    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"

    NotFound:
      description: Resource not found (or not owned by the authenticated user).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"

    PlanLimit:
      description: Plan limit reached. Upgrade your plan to continue.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"

    InternalError:
      description: Internal server error or AI generation failure.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"

  schemas:

    # ── Shared primitives ────────────────────────────────────

    ErrorDetail:
      type: object
      required: [code, message]
      properties:
        code:
          type: string
          description: Machine-readable error code.
          examples:
            - VALIDATION
            - NOT_FOUND
            - PLAN_LIMIT
            - GENERATION_FAILED
            - PARSE_ERROR
            - MISSING_DATA
        message:
          type: string
          description: Human-readable explanation.

    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          $ref: "#/components/schemas/ErrorDetail"

    SuccessResponse:
      type: object
      required: [success]
      properties:
        success:
          type: boolean
          const: true

    # ── Enums ────────────────────────────────────────────────

    SectionType:
      type: string
      description: Section template type.
      enum:
        - hero
        - features-grid
        - features-list
        - social-proof
        - testimonials
        - pricing
        - faq
        - cta
        - stats
        - text-block
        - pain
        - how-it-works
        - results
        - comparison-table
        - showcase
        - news
        - code-sample
        - founder-story
        - statement
        - screenshot

    VariationTone:
      type: string
      description: Copy tone / style.
      enum:
        - punchy
        - conversational
        - benefit-focused
        - problem-aware
        - bold-confident

    PageType:
      type: string
      description: The page template / purpose.
      enum:
        - homepage
        - product
        - service
        - pricing
        - customer-stories

    # ── API Key ──────────────────────────────────────────────

    ApiKey:
      type: object
      description: Metadata about an API key (the plaintext key is only shown once on creation).
      required: [id, name, keyPrefix, createdAt]
      properties:
        id:
          type: string
        name:
          type: string
          description: Human label for the key (e.g. "CI pipeline key").
        keyPrefix:
          type: string
          description: First 8 characters of the key for display (e.g. `pp_live_a`).
        lastUsedAt:
          type: string
          format: date-time
          nullable: true
        revokedAt:
          type: string
          format: date-time
          nullable: true
        createdAt:
          type: string
          format: date-time

    # ── Projects ─────────────────────────────────────────────

    ProjectSummary:
      type: object
      required: [id, name, language, createdAt, updatedAt]
      properties:
        id:
          type: string
        name:
          type: string
        description:
          type: string
          nullable: true
        url:
          type: string
          nullable: true
        language:
          type: string
          description: BCP 47 language code (e.g. `en`, `nl`). Default `en`.
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    ProjectDetail:
      allOf:
        - $ref: "#/components/schemas/ProjectSummary"
        - type: object
          properties:
            knowledgeBase:
              type: string
              nullable: true
              description: Raw Markdown knowledge base used to inform AI generation.
            kbLastUpdated:
              type: string
              format: date-time
              nullable: true

    ProjectCreated:
      allOf:
        - $ref: "#/components/schemas/ProjectSummary"
        - type: object
          required: [homepageId]
          properties:
            homepageId:
              type: string
              description: ID of the auto-created "Homepage" page.

    CreateProjectRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string
          maxLength: 200
          description: Project name.
        description:
          type: string
          maxLength: 1000
          nullable: true
        url:
          type: string
          maxLength: 500
          nullable: true
          description: Product or marketing website URL.
        language:
          type: string
          maxLength: 10
          nullable: true
          description: BCP 47 language code for AI output (e.g. `en`, `nl`).
        knowledgeBase:
          type: string
          maxLength: 50000
          nullable: true
          description: Initial knowledge base Markdown (max 50 KB).

    UpdateProjectRequest:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 200
        description:
          type: string
          maxLength: 1000
          nullable: true
        url:
          type: string
          maxLength: 500
          nullable: true
        language:
          type: string
          maxLength: 10
          nullable: true
        knowledgeBase:
          type: string
          maxLength: 50000
          nullable: true

    # ── Knowledge Base ───────────────────────────────────────

    KnowledgeBaseResponse:
      type: object
      required: [markdown]
      properties:
        markdown:
          type: string
          description: Markdown content of the knowledge base. Empty string if not set.
        lastUpdated:
          type: string
          format: date-time
          nullable: true

    UpdateKnowledgeBaseRequest:
      type: object
      required: [markdown]
      properties:
        markdown:
          type: string
          maxLength: 50000
          description: Full Markdown replacement for the knowledge base.

    ExtractKnowledgeRequest:
      type: object
      description: At least one of `url` or `context` must be provided.
      properties:
        url:
          type: string
          description: A publicly accessible product/marketing URL to scrape.
        context:
          type: string
          description: Raw text about the product (e.g. copy-pasted from a README or pitch deck).

    ExtractKnowledgeSuccess:
      type: object
      required: [markdown]
      properties:
        markdown:
          type: string
          description: AI-extracted knowledge base as Markdown.

    NeedsManualInput:
      type: object
      required: [status, reason]
      properties:
        status:
          type: string
          const: needs_manual_input
        reason:
          type: string
          enum: [scrape_failed, insufficient_content]

    # ── Pages ────────────────────────────────────────────────

    PageSummary:
      type: object
      required: [id, name, slug, wizardCompleted, order, isPlaced, createdAt, updatedAt]
      properties:
        id:
          type: string
        name:
          type: string
        slug:
          type: string
          description: URL-safe identifier unique within the project.
        wizardCompleted:
          type: boolean
        parentId:
          type: string
          nullable: true
          description: Parent page ID for sitemap tree nesting.
        order:
          type: integer
          description: Sort order within siblings.
        isPlaced:
          type: boolean
          description: Whether the page is placed in the sitemap tree.
        shareToken:
          type: string
          nullable: true
        shareUrl:
          type: string
          nullable: true
          description: Public share URL (`https://piratepage.cc/p/{shareToken}`).
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    PageDetail:
      allOf:
        - $ref: "#/components/schemas/PageSummary"
        - type: object
          properties:
            wizardAnswers:
              $ref: "#/components/schemas/WizardAnswers"
              nullable: true
            sections:
              type: array
              items:
                $ref: "#/components/schemas/Section"
            sectionVariations:
              type: object
              description: Map of sectionId → array of variations.
              additionalProperties:
                type: array
                items:
                  $ref: "#/components/schemas/SectionVariation"
            generationChoices:
              type: array
              items:
                type: string
              description: AI explanations of structural decisions made during generation.

    PageCreated:
      type: object
      required: [id, name, slug, wizardCompleted, order, createdAt, updatedAt]
      properties:
        id:
          type: string
        name:
          type: string
        slug:
          type: string
        parentId:
          type: string
          nullable: true
        order:
          type: integer
        wizardCompleted:
          type: boolean
        shareToken:
          type: string
          nullable: true
        shareUrl:
          type: string
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    PageUpdated:
      type: object
      required: [id, name, slug, wizardCompleted, createdAt, updatedAt]
      properties:
        id:
          type: string
        name:
          type: string
        slug:
          type: string
        wizardCompleted:
          type: boolean
        shareToken:
          type: string
          nullable: true
        shareUrl:
          type: string
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    CreatePageRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string
          maxLength: 200
        parentId:
          type: string
          nullable: true
          description: Parent page ID for sitemap tree nesting.

    UpdatePageRequest:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 200
        promptVersion:
          type: integer
          nullable: true
          description: Prompt version override. Omit to use the latest version.

    DuplicatePageRequest:
      type: object
      properties:
        name:
          type: string
          maxLength: 200
          description: Name for the duplicate. Defaults to `"<original name> (copy)"`.

    ReorderPagesRequest:
      type: object
      required: [pages]
      properties:
        pages:
          type: array
          maxItems: 200
          items:
            type: object
            required: [id, order]
            properties:
              id:
                type: string
              parentId:
                type: string
                nullable: true
              order:
                type: integer
              isPlaced:
                type: boolean

    ShareLinkResponse:
      type: object
      required: [shareToken, shareUrl]
      properties:
        shareToken:
          type: string
        shareUrl:
          type: string

    ExportPageResponse:
      type: object
      required: [name, format, markdown, sections]
      properties:
        name:
          type: string
          description: Page name.
        format:
          type: string
          enum: [flat, ascii]
        markdown:
          type: string
          description: Full page as Markdown.
        sections:
          type: integer
          description: Number of sections exported.
        shareUrl:
          type: string
          nullable: true

    # ── Wizard ───────────────────────────────────────────────

    WizardAnswers:
      type: object
      description: |
        The 9-question positioning wizard answers. All fields are strings.
        `productName` through `userFears` are the core questions. Additional
        fields provide extra context for specific page types.
      properties:
        pageType:
          $ref: "#/components/schemas/PageType"
        productName:
          type: string
          description: What is your product called?
        whatItIs:
          type: string
          description: What IS your product?
        whatItIsNot:
          type: string
          description: What is your product NOT?
        keyTakeaway:
          type: string
          description: What's the key takeaway?
        wordOfMouth:
          type: string
          description: How would an excited user describe this to a friend?
        competitors:
          type: string
          description: Who are your competitors?
        differentiation:
          type: string
          description: How are you different or better?
        userMotivations:
          type: string
          description: What motivates your target users?
        userFears:
          type: string
          description: What fears or objections do your users have?
        currentUrl:
          type: string
          nullable: true
          description: Product URL (used for context scraping on regeneration).
        rawContext:
          type: string
          nullable: true
          description: Additional raw text context.
        features:
          type: string
          nullable: true
          description: Key features list.
        testimonials:
          type: string
          nullable: true
          description: Customer quotes.
        emphasis:
          type: string
          nullable: true
          description: Anything to emphasise in generation.
        usabilityInsights:
          type: string
          nullable: true
          description: Usability test insights.
        pricingTiers:
          type: string
          nullable: true
          description: Pricing tier descriptions (for pricing page type).
        serviceProcess:
          type: string
          nullable: true
          description: Service process steps (for service page type).
        customerName:
          type: string
          nullable: true
          description: Featured customer name and company (for customer-stories type).
        customerChallenge:
          type: string
          nullable: true
          description: Customer challenge before the product (for customer-stories type).
        customerOutcome:
          type: string
          nullable: true
          description: Results the customer achieved (for customer-stories type).

    SaveWizardAnswersRequest:
      type: object
      required: [answers]
      properties:
        answers:
          $ref: "#/components/schemas/WizardAnswers"
        language:
          type: string
          maxLength: 10
          nullable: true
          description: BCP 47 language code for AI output.

    SaveWizardAnswersResponse:
      type: object
      required: [id, wizardCompleted, wizardAnswers]
      properties:
        id:
          type: string
        wizardCompleted:
          type: boolean
        wizardAnswers:
          $ref: "#/components/schemas/WizardAnswers"

    ExtractWizardAnswersRequest:
      type: object
      description: At least one of `url` or `context` must be provided.
      properties:
        url:
          type: string
          description: Product URL to scrape.
        context:
          type: string
          description: Raw text to extract answers from.
        pageType:
          $ref: "#/components/schemas/PageType"
          description: Page type hint to guide extraction.

    ExtractWizardAnswersSuccess:
      type: object
      required: [answers]
      properties:
        answers:
          $ref: "#/components/schemas/WizardAnswers"

    # ── Generation ───────────────────────────────────────────

    GeneratePageRequest:
      type: object
      properties:
        feedback:
          type: string
          maxLength: 1000
          nullable: true
          description: Optional regeneration instructions (only used when the page already has sections).

    GeneratePageResponse:
      type: object
      required: [id, sections, generationChoices, shareToken, shareUrl, durationMs]
      properties:
        id:
          type: string
          description: Page ID.
        sections:
          type: array
          items:
            $ref: "#/components/schemas/Section"
        generationChoices:
          type: array
          items:
            type: string
          description: AI explanations for structural decisions.
        shareToken:
          type: string
        shareUrl:
          type: string
        durationMs:
          type: integer
          description: Total end-to-end duration in milliseconds.

    # ── Sections ─────────────────────────────────────────────

    Section:
      type: object
      required: [id, type, order, content]
      properties:
        id:
          type: string
          description: Section identifier (e.g. `section-hero-ab12`).
        type:
          $ref: "#/components/schemas/SectionType"
        order:
          type: integer
          description: 0-based position in the page.
        content:
          type: object
          description: |
            Section content. Shape varies by `type`. See the SectionContent schemas
            for per-type field documentation.
          additionalProperties: true
        isInvented:
          type: boolean
          nullable: true
          description: True for sections whose content is always AI-invented (testimonials, stats, social-proof).
        isEdited:
          type: boolean
          nullable: true
          description: True if the section has been manually edited after generation.

    ReplaceSectionsRequest:
      type: object
      required: [sections]
      properties:
        sections:
          type: array
          maxItems: 50
          items:
            $ref: "#/components/schemas/Section"

    ReplaceSectionsResponse:
      type: object
      required: [sections]
      properties:
        sections:
          type: array
          items:
            $ref: "#/components/schemas/Section"

    AddSectionRequest:
      type: object
      required: [type]
      properties:
        type:
          $ref: "#/components/schemas/SectionType"
        content:
          type: object
          additionalProperties: true
          description: Pre-populated content. Use an empty object `{}` to start blank.
        variant:
          type: string
          nullable: true
          description: Section variant (e.g. `with-image`, `bento`). Type-dependent.
        insertAt:
          type: integer
          minimum: 0
          nullable: true
          description: 0-based index to insert at. Omit to append at the end.

    UpdateSectionRequest:
      type: object
      required: [content]
      properties:
        content:
          type: object
          additionalProperties: true
          description: Full replacement content object for the section.

    MoveSectionRequest:
      type: object
      required: [direction]
      properties:
        direction:
          type: string
          enum: [up, down]

    MoveSectionResponse:
      type: object
      required: [sections]
      properties:
        sections:
          type: array
          items:
            $ref: "#/components/schemas/Section"

    RepromptSectionRequest:
      type: object
      required: [instruction]
      properties:
        instruction:
          type: string
          maxLength: 1000
          description: |
            Natural language instruction for rewriting the section. Examples:
            "make it more conversational", "emphasise the time saving", "add urgency".

    RepromptSectionResponse:
      type: object
      required: [content, variations]
      properties:
        content:
          type: object
          additionalProperties: true
          description: The new section content.
        variations:
          type: array
          items:
            $ref: "#/components/schemas/SectionVariation"
          description: All variations for this section after the reprompt (including Original and new).

    # ── Variations ───────────────────────────────────────────

    SectionVariation:
      type: object
      required: [id, content, createdAt, isDiscarded]
      properties:
        id:
          type: string
          description: Variation identifier.
        content:
          type: object
          additionalProperties: true
          description: Section content for this variation.
        createdAt:
          type: string
          format: date-time
        instruction:
          type: string
          nullable: true
          description: |
            Label describing how this variation was created. Special values:
            `"Original"` (the baseline at time of generation/reprompt),
            `"Style: punchy"` etc. for tone variations.
        isDiscarded:
          type: boolean

    ListVariationsResponse:
      type: object
      required: [variations]
      properties:
        variations:
          type: array
          items:
            $ref: "#/components/schemas/SectionVariation"

    GenerateVariationsResponse:
      type: object
      required: [message, variations]
      properties:
        message:
          type: string
          description: Human-readable summary (e.g. `"Generated 5 variations"`).
        variations:
          type: array
          items:
            $ref: "#/components/schemas/SectionVariation"

    ManageVariationRequest:
      type: object
      required: [action]
      properties:
        action:
          type: string
          enum: [select, discard, restore]
          description: |
            `select` — apply variation content to the live section.
            `discard` — soft-hide the variation.
            `restore` — un-hide a discarded variation.

    ManageVariationResponse:
      type: object
      required: [message]
      properties:
        message:
          type: string
        content:
          type: object
          additionalProperties: true
          nullable: true
          description: Present (and equal to the variation's content) when action is `select`.
        variation:
          $ref: "#/components/schemas/SectionVariation"
          nullable: true
          description: Present (with updated `isDiscarded`) when action is `discard` or `restore`.

    # ── Section content schemas (reference documentation) ────
    # These are informational; the API accepts any valid object for `content`.

    CTAButton:
      type: object
      required: [text]
      properties:
        text:
          type: string
        href:
          type: string
          nullable: true

    MiniProof:
      type: object
      required: [type, content]
      properties:
        type:
          type: string
          enum: [quote, stat, badge]
        content:
          type: string
        author:
          type: string
          nullable: true

    HeroContent:
      type: object
      required: [headline, subheadline, primaryCTA]
      properties:
        eyebrow:
          type: string
          nullable: true
        pill:
          type: string
          nullable: true
        headline:
          type: string
        subheadline:
          type: string
        primaryCTA:
          $ref: "#/components/schemas/CTAButton"
        secondaryCTA:
          $ref: "#/components/schemas/CTAButton"
          nullable: true
        variant:
          type: string
          enum: [default, with-image, with-proof, with-screenshot, with-checklist]
          nullable: true
        image:
          type: string
          nullable: true
        socialProof:
          type: string
          nullable: true
        rating:
          type: string
          nullable: true
        reviewCount:
          type: string
          nullable: true
        trustBadge:
          type: string
          nullable: true
        featuredQuote:
          type: object
          nullable: true
          properties:
            quote:
              type: string
            author:
              type: string
            role:
              type: string
              nullable: true
        checklist:
          type: array
          nullable: true
          items:
            type: string
        miniProof:
          $ref: "#/components/schemas/MiniProof"
          nullable: true

    Feature:
      type: object
      required: [title, description]
      properties:
        title:
          type: string
        description:
          type: string
        image:
          type: string
          nullable: true
        icon:
          type: string
          nullable: true
        quote:
          type: string
          nullable: true
        quoteAuthor:
          type: string
          nullable: true

    FeaturesGridContent:
      type: object
      required: [features, columns]
      properties:
        eyebrow:
          type: string
          nullable: true
        headline:
          type: string
          nullable: true
        subheadline:
          type: string
          nullable: true
        features:
          type: array
          items:
            $ref: "#/components/schemas/Feature"
        columns:
          type: integer
          enum: [2, 3]
        variant:
          type: string
          enum: [default, with-images, bento, icon-grid, showcase, tabs]
          nullable: true
        miniProof:
          $ref: "#/components/schemas/MiniProof"
          nullable: true

    FeaturesListContent:
      type: object
      required: [features]
      properties:
        eyebrow:
          type: string
          nullable: true
        headline:
          type: string
          nullable: true
        features:
          type: array
          items:
            $ref: "#/components/schemas/Feature"
        variant:
          type: string
          enum: [default, spotlight, highlight]
          nullable: true
        miniProof:
          $ref: "#/components/schemas/MiniProof"
          nullable: true

    SocialProofContent:
      type: object
      required: [logos]
      properties:
        eyebrow:
          type: string
          nullable: true
        headline:
          type: string
          nullable: true
        logos:
          type: array
          items:
            type: string
        badges:
          type: array
          nullable: true
          items:
            type: string
        variant:
          type: string
          enum: [default]
          nullable: true
        miniProof:
          $ref: "#/components/schemas/MiniProof"
          nullable: true

    Testimonial:
      type: object
      required: [quote, author]
      properties:
        quote:
          type: string
        author:
          type: string
        role:
          type: string
          nullable: true
        image:
          type: string
          nullable: true
        metric:
          type: string
          nullable: true

    TestimonialsContent:
      type: object
      required: [testimonials]
      properties:
        eyebrow:
          type: string
          nullable: true
        headline:
          type: string
          nullable: true
        testimonials:
          type: array
          items:
            $ref: "#/components/schemas/Testimonial"
        variant:
          type: string
          enum: [default, featured, with-results]
          nullable: true
        miniProof:
          $ref: "#/components/schemas/MiniProof"
          nullable: true

    PricingPlan:
      type: object
      required: [name, price, features, cta]
      properties:
        name:
          type: string
        price:
          type: string
        features:
          type: array
          items:
            type: string
        cta:
          $ref: "#/components/schemas/CTAButton"
        highlighted:
          type: boolean
          nullable: true
        badge:
          type: string
          nullable: true

    PricingContent:
      type: object
      required: [plans]
      properties:
        eyebrow:
          type: string
          nullable: true
        headline:
          type: string
          nullable: true
        plans:
          type: array
          items:
            $ref: "#/components/schemas/PricingPlan"
        variant:
          type: string
          enum: [default, single]
          nullable: true
        guarantee:
          type: string
          nullable: true
        miniProof:
          $ref: "#/components/schemas/MiniProof"
          nullable: true

    FAQItem:
      type: object
      required: [question, answer]
      properties:
        question:
          type: string
        answer:
          type: string

    FAQContent:
      type: object
      required: [faqs]
      properties:
        eyebrow:
          type: string
          nullable: true
        headline:
          type: string
          nullable: true
        faqs:
          type: array
          items:
            $ref: "#/components/schemas/FAQItem"
        variant:
          type: string
          enum: [default]
          nullable: true
        miniProof:
          $ref: "#/components/schemas/MiniProof"
          nullable: true

    CTAContent:
      type: object
      required: [headline, primaryCTA]
      properties:
        eyebrow:
          type: string
          nullable: true
        headline:
          type: string
        subheadline:
          type: string
          nullable: true
        primaryCTA:
          $ref: "#/components/schemas/CTAButton"
        variant:
          type: string
          enum: [default, with-proof, with-trust]
          nullable: true
        trustBadges:
          type: array
          nullable: true
          items:
            type: string
        miniProof:
          $ref: "#/components/schemas/MiniProof"
          nullable: true

    Stat:
      type: object
      required: [value, label]
      properties:
        value:
          type: string
        label:
          type: string
        context:
          type: string
          nullable: true

    StatsContent:
      type: object
      required: [stats]
      properties:
        eyebrow:
          type: string
          nullable: true
        headline:
          type: string
          nullable: true
        stats:
          type: array
          items:
            $ref: "#/components/schemas/Stat"
        variant:
          type: string
          enum: [default, with-context, proof-bar]
          nullable: true
        proofStat:
          type: string
          nullable: true
        miniProof:
          $ref: "#/components/schemas/MiniProof"
          nullable: true

    TextBlockContent:
      type: object
      required: [content]
      properties:
        eyebrow:
          type: string
          nullable: true
        headline:
          type: string
          nullable: true
        content:
          type: string
        image:
          type: string
          nullable: true
        miniProof:
          $ref: "#/components/schemas/MiniProof"
          nullable: true

    PainContent:
      type: object
      required: [headline]
      properties:
        eyebrow:
          type: string
          nullable: true
        headline:
          type: string
        subheadline:
          type: string
          nullable: true
        painPoints:
          type: array
          nullable: true
          items:
            type: object
            required: [title, description]
            properties:
              title:
                type: string
              description:
                type: string
        variant:
          type: string
          enum: [default, before-after]
          nullable: true
        before:
          type: object
          nullable: true
          properties:
            headline:
              type: string
            points:
              type: array
              items:
                type: string
        after:
          type: object
          nullable: true
          properties:
            headline:
              type: string
            points:
              type: array
              items:
                type: string
        miniProof:
          $ref: "#/components/schemas/MiniProof"
          nullable: true

    HowItWorksContent:
      type: object
      required: [headline, steps]
      properties:
        eyebrow:
          type: string
          nullable: true
        headline:
          type: string
        steps:
          type: array
          items:
            type: object
            required: [title, description]
            properties:
              title:
                type: string
              description:
                type: string
              image:
                type: string
                nullable: true
        variant:
          type: string
          enum: [default, with-images]
          nullable: true
        miniProof:
          $ref: "#/components/schemas/MiniProof"
          nullable: true

    ResultsContent:
      type: object
      required: [headline]
      properties:
        eyebrow:
          type: string
          nullable: true
        headline:
          type: string
        variant:
          type: string
          enum: [metrics, story]
          nullable: true
        results:
          type: array
          nullable: true
          items:
            type: object
            required: [value, context]
            properties:
              value:
                type: string
              context:
                type: string
        story:
          type: object
          nullable: true
          properties:
            customer:
              type: string
            beforeTitle:
              type: string
              nullable: true
            before:
              type: string
            afterTitle:
              type: string
              nullable: true
            after:
              type: string
            quote:
              type: string
              nullable: true
            quoteAuthor:
              type: string
              nullable: true
        miniProof:
          $ref: "#/components/schemas/MiniProof"
          nullable: true

    ComparisonTableContent:
      type: object
      required: [plans, categories]
      properties:
        eyebrow:
          type: string
          nullable: true
        headline:
          type: string
          nullable: true
        plans:
          type: array
          items:
            type: string
          description: Column headers (plan names).
        categories:
          type: array
          items:
            type: object
            required: [name, features]
            properties:
              name:
                type: string
              features:
                type: array
                items:
                  type: object
                  required: [name, values]
                  properties:
                    name:
                      type: string
                    description:
                      type: string
                      nullable: true
                    values:
                      type: object
                      additionalProperties:
                        oneOf:
                          - type: string
                          - type: boolean
        highlightedPlan:
          type: string
          nullable: true
        miniProof:
          $ref: "#/components/schemas/MiniProof"
          nullable: true

    ShowcaseContent:
      type: object
      required: [items]
      properties:
        eyebrow:
          type: string
          nullable: true
        headline:
          type: string
          nullable: true
        subheadline:
          type: string
          nullable: true
        items:
          type: array
          items:
            type: object
            required: [title]
            properties:
              title:
                type: string
              description:
                type: string
                nullable: true
              image:
                type: string
                nullable: true
              url:
                type: string
                nullable: true
              badge:
                type: string
                nullable: true
        variant:
          type: string
          enum: [grid, cards, logo-cards]
          nullable: true
        miniProof:
          $ref: "#/components/schemas/MiniProof"
          nullable: true

    NewsContent:
      type: object
      required: [items]
      properties:
        eyebrow:
          type: string
          nullable: true
        headline:
          type: string
          nullable: true
        items:
          type: array
          items:
            type: object
            required: [date, title, description]
            properties:
              date:
                type: string
              title:
                type: string
              description:
                type: string
              badge:
                type: string
                nullable: true
              url:
                type: string
                nullable: true
        variant:
          type: string
          enum: [default, timeline, cards]
          nullable: true
        miniProof:
          $ref: "#/components/schemas/MiniProof"
          nullable: true

    CodeSampleContent:
      type: object
      required: [blocks]
      properties:
        eyebrow:
          type: string
          nullable: true
        headline:
          type: string
          nullable: true
        subheadline:
          type: string
          nullable: true
        blocks:
          type: array
          items:
            type: object
            required: [language, code]
            properties:
              language:
                type: string
                description: Language identifier for syntax highlighting (e.g. `typescript`, `bash`).
              label:
                type: string
                nullable: true
              code:
                type: string
        variant:
          type: string
          enum: [default, tabs]
          nullable: true
        miniProof:
          $ref: "#/components/schemas/MiniProof"
          nullable: true

    FounderStoryContent:
      type: object
      required: [headline, story, founderName]
      properties:
        eyebrow:
          type: string
          nullable: true
        headline:
          type: string
        story:
          type: string
        founderName:
          type: string
        founderRole:
          type: string
          nullable: true
        founderImage:
          type: string
          nullable: true
        signature:
          type: string
          nullable: true
        miniProof:
          $ref: "#/components/schemas/MiniProof"
          nullable: true

    StatementContent:
      type: object
      required: [headline]
      properties:
        eyebrow:
          type: string
          nullable: true
        headline:
          type: string
          description: Large-type statement. Wrap portions in `**double asterisks**` for black emphasis; remainder renders in gray.
        miniProof:
          $ref: "#/components/schemas/MiniProof"
          nullable: true

    ScreenshotContent:
      type: object
      required: [headline, image]
      properties:
        eyebrow:
          type: string
          nullable: true
        headline:
          type: string
        description:
          type: string
          nullable: true
        image:
          type: string
          description: URL or path to the screenshot image.
        benefits:
          type: array
          nullable: true
          items:
            type: object
            required: [title, description]
            properties:
              icon:
                type: string
                nullable: true
              title:
                type: string
              description:
                type: string
        variant:
          type: string
          enum: [default, with-benefits]
          nullable: true
        miniProof:
          $ref: "#/components/schemas/MiniProof"
          nullable: true
