{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://cratly.io/schema/redirects/v0.json",
  "title": "Cratly redirect manifest",
  "description": "Grammar for a redirect manifest (conventionally written to `cratly-redirects.json` in a site's build output by a theme framework adapter such as Scavold). It lists the addresses a website inherited from the site it replaced, where each of them now leads, and which HTTP status says so. The format names no web server on purpose: only a server-side redirect passes a search ranking on, and cratly does not assume who serves a site — a deploy step reads this file and writes what its target understands (an `.s3-http.config.yaml`, a Caddy or nginx rule set, a bucket's routing rules, a `_redirects` file). This file is framework-agnostic: any adapter may produce a conforming manifest. The grammar is a stable, versioned contract — tools SHOULD bundle the version(s) they understand rather than fetch this document at runtime.",
  "type": "object",
  "required": [ "specVersion", "rules" ],
  "additionalProperties": false,
  "properties": {
    "$schema": {
      "type": "string",
      "description": "Optional pointer back to this grammar. When present it SHOULD be the canonical URL of the grammar version the manifest conforms to."
    },
    "specVersion": {
      "type": "integer",
      "description": "Version of this grammar the manifest conforms to. Tools reject or warn on versions they do not support.",
      "enum": [ 0 ]
    },
    "adapter": {
      "type": "object",
      "description": "Identifies the tool that produced this manifest. Informational; a consumer does not need it to apply the rules.",
      "additionalProperties": true,
      "properties": {
        "name": { "type": "string", "description": "Adapter identifier, e.g. \"scavold\"." },
        "version": { "type": "string", "description": "Adapter package version that generated this manifest." }
      }
    },
    "rules": {
      "type": "array",
      "description": "The inherited addresses, sorted by `from` so the file changes only when the declarations do. Consumers SHOULD evaluate them before looking for a file, and MUST treat the first matching rule as the answer.",
      "items": { "$ref": "#/$defs/rule" }
    }
  },
  "$defs": {
    "rule": {
      "type": "object",
      "required": [ "from", "status" ],
      "additionalProperties": false,
      "properties": {
        "from": {
          "type": "string",
          "pattern": "^/",
          "description": "The inherited address, site-absolute and as it stood in the browser — extension and all (`/kontakt.php`). A query string may be present; whether a consumer can match on it is up to that consumer, and one that cannot SHOULD ignore the rule rather than match the path alone. No fragment: it never reaches a server."
        },
        "to": {
          "type": "string",
          "description": "Where the address now leads: site-absolute, naming the file the page is built into (`/de/kontakt.html`), or an absolute URL when the content moved to another host. Required for a redirecting status, absent for `410`.",
          "minLength": 1
        },
        "status": {
          "type": "integer",
          "description": "The answer a server gives. `301` — moved permanently, the only status that passes an accumulated search ranking on, and the default for an inherited address. `308` — same, for the rare case that the request method must survive. `410` — gone: the address is not coming back and a search engine should drop it rather than keep asking; carries no `to`.",
          "enum": [ 301, 308, 410 ]
        }
      },
      "allOf": [
        {
          "if": { "properties": { "status": { "enum": [ 301, 308 ] } }, "required": [ "status" ] },
          "then": { "required": [ "to" ] }
        },
        {
          "if": { "properties": { "status": { "const": 410 } }, "required": [ "status" ] },
          "then": { "not": { "required": [ "to" ] } }
        }
      ]
    }
  }
}
