{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://novanode.eu/schemas/web/export-1.json",
  "title": "NovaNode Sites export, version 1",
  "description": "Structure of the ZIP archive a customer downloads from the NovaNode Sites editor (Settings, Export) and of its manifest.json. This is the open, machine-readable format referred to in the Data Structures and Formats Register at https://novanode.eu/legal/switching (Terms of Service 5.4.3; Regulation (EU) 2023/2854, Article 30(5)). The archive contains the customer's own content, media and data only; the NovaNode Sites editor, templates, themes, the HTML/CSS/JavaScript generated by the platform and image renditions are not part of it.",
  "$comment": "Archive layout (all paths relative to the ZIP root): manifest.json (this schema); README.txt; content/site.json (https://novanode.eu/schemas/web/site-1.json); content/pages.json; content/posts.json; content/settings.json; media/index.json; media/files/<public_id>.<ext> (original uploads, byte for byte); data/form-submissions.csv; data/form-uploads/<submission_id>/<filename>; data/appointments.csv; data/newsletter-subscribers.csv; data/newsletter-sends.csv; data/redirects.csv; domains.json. CSV files follow RFC 4180, UTF-8 with BOM, ISO 8601 UTC timestamps, header row always present. Localised strings in the JSON files are objects keyed by language code, e.g. {\"fr\": \"Accueil\", \"en\": \"Home\"}. Pages, blocks and media are referenced by their public_id; internal numeric ids are never exported.",
  "type": "object",
  "required": ["format", "version", "created_at", "site", "schemas", "files"],
  "additionalProperties": false,
  "properties": {
    "format": { "const": "novanode-sites-export" },
    "version": { "const": 1 },
    "created_at": { "type": "string", "format": "date-time", "description": "When the archive was generated (ISO 8601, UTC)." },
    "site": {
      "type": "object",
      "required": ["tier", "languages", "default_language"],
      "additionalProperties": false,
      "properties": {
        "tier": { "type": "string", "enum": ["link", "basic", "business"], "description": "Plan at export time: Link, Basic or Business." },
        "tier_label": { "type": "string", "description": "Plan name as shown to the customer (Link, Basic, Business)." },
        "name": { "$ref": "#/$defs/localised" },
        "languages": { "type": "array", "minItems": 1, "maxItems": 5, "items": { "$ref": "#/$defs/lang" }, "description": "Languages the site is published in." },
        "default_language": { "$ref": "#/$defs/lang" }
      }
    },
    "schemas": {
      "type": "object",
      "description": "Schema URL for each JSON file in the archive that has one.",
      "properties": {
        "content/site.json": { "const": "https://novanode.eu/schemas/web/site-1.json" }
      },
      "additionalProperties": { "type": "string", "format": "uri" }
    },
    "files": {
      "type": "object",
      "description": "Every file in the archive except manifest.json, keyed by its path, with the SHA-256 of its contents in lower-case hex.",
      "propertyNames": { "pattern": "^(README\\.txt|content/[a-z-]+\\.json|media/index\\.json|media/files/[a-z0-9]+\\.[A-Za-z0-9]+|data/[a-z-]+\\.csv|data/form-uploads/[^/]+/[^/]+|domains\\.json)$" },
      "additionalProperties": { "type": "string", "pattern": "^[0-9a-f]{64}$" }
    },
    "excluded": {
      "type": "array",
      "items": { "type": "string" },
      "description": "Categories deliberately not included, as listed in the Register: platform software and generated output (editor, templates, themes, generated HTML/CSS/JS, image renditions), platform configuration, internal statistics, abuse and security tooling data, and credentials (password hashes, API tokens, session data)."
    }
  },
  "$defs": {
    "lang": { "type": "string", "enum": ["en", "fr", "de", "nl", "lb"] },
    "localised": {
      "type": "object",
      "description": "A text in one or more site languages.",
      "propertyNames": { "enum": ["en", "fr", "de", "nl", "lb"] },
      "additionalProperties": { "type": "string" }
    },
    "pages_json": {
      "description": "content/pages.json: flat page index.",
      "type": "array",
      "items": {
        "type": "object",
        "required": ["public_id", "slug", "title"],
        "properties": {
          "public_id": { "type": "string" },
          "slug": { "type": "string" },
          "role": { "type": "string", "description": "Page role where the editor assigns one (home, contact, legal, ...)." },
          "title": { "$ref": "#/$defs/localised" },
          "nav": { "type": "boolean", "description": "Shown in the site navigation." },
          "published": { "type": "boolean" },
          "protected": { "type": "boolean", "description": "True when the page is password-protected; the password itself is not exported." },
          "seo": { "type": "object", "properties": { "title": { "$ref": "#/$defs/localised" }, "description": { "$ref": "#/$defs/localised" }, "noindex": { "type": "boolean" } } }
        }
      }
    },
    "posts_json": {
      "description": "content/posts.json: blog posts (news and event entries) lifted out of their blocks, so the blog is usable without the block format.",
      "type": "array",
      "items": {
        "type": "object",
        "required": ["page_public_id", "block_public_id", "title"],
        "properties": {
          "page_public_id": { "type": "string" },
          "block_public_id": { "type": "string" },
          "kind": { "type": "string", "enum": ["news", "event"] },
          "title": { "$ref": "#/$defs/localised" },
          "date": { "type": "string", "format": "date" },
          "starts_at": { "type": "string", "format": "date-time" },
          "ends_at": { "type": "string", "format": "date-time" },
          "body": { "$ref": "#/$defs/localised" },
          "image": { "type": "string", "description": "public_id of a file in media/files, or absent." }
        }
      }
    },
    "settings_json": {
      "description": "content/settings.json: theme, layout, languages, footer, organisation, SEO defaults, consent configuration, 404 and coming-soon pages, the customer's own head code and per-page look. Credentials (mail password, API keys) are stripped.",
      "type": "object"
    },
    "media_index_json": {
      "description": "media/index.json: one entry per original file in media/files.",
      "type": "array",
      "items": {
        "type": "object",
        "required": ["public_id", "file", "original_name", "mime", "bytes", "sha256"],
        "properties": {
          "public_id": { "type": "string" },
          "file": { "type": "string", "description": "Path inside the archive, media/files/<public_id>.<ext>." },
          "original_name": { "type": "string" },
          "mime": { "type": "string" },
          "bytes": { "type": "integer", "minimum": 0 },
          "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
          "width": { "type": "integer" },
          "height": { "type": "integer" },
          "alt": { "$ref": "#/$defs/localised" }
        }
      }
    },
    "domains_json": {
      "description": "domains.json: connected domain names and their configuration.",
      "type": "array",
      "items": {
        "type": "object",
        "required": ["hostname", "primary"],
        "properties": {
          "hostname": { "type": "string" },
          "primary": { "type": "boolean" },
          "verification": { "type": "string", "description": "How the domain was verified (dns, http, registrar)." },
          "status": { "type": "string" }
        }
      }
    },
    "csv": {
      "description": "Column sets of the CSV files in data/. Column order as listed; a header row is always present, even for an empty file.",
      "type": "object",
      "properties": {
        "form-submissions.csv": { "const": "submission_id, block_id, page_slug, received_at, status, answered_at, answer_note, then one column per form field (the union of every field name ever used on that block); file attachments are stored under data/form-uploads/<submission_id>/ and the cell holds the relative path" },
        "appointments.csv": { "const": "id, block_id, service, starts_at, ends_at, minutes, name, email, phone, note, lang, status, cancelled_by, created_at" },
        "newsletter-subscribers.csv": { "const": "email, name, lang, status, confirmed_at, unsubscribed_at, created_at" },
        "newsletter-sends.csv": { "const": "subject, body, button_label, button_url, lang, status, recipients, sent, finished_at" },
        "redirects.csv": { "const": "path, target, hits" }
      }
    }
  }
}
