Example policies

Pass any of these policies to ContentProtection.configure({ policy }). The named Nodes, Marks and attributes must exist in your editor's schema.

import type { ContentProtectionPolicy } from '@tiptap-pro/extension-content-protection'

Protect headings

const policy = {
  version: 1,
  rules: [
    {
      selector: { target: 'node', node: { types: ['heading'] } },
      permissions: { edit: false },
    },
  ],
} satisfies ContentProtectionPolicy

Protects headings, including their type, text, formatting and attributes. Use target: 'nodeType' instead to keep their structure fixed while allowing text and attribute edits.

Fix heading levels

const policy = {
  version: 1,
  rules: [
    {
      selector: {
        target: 'attribute',
        node: { types: ['heading'] },
        names: ['level'],
      },
      permissions: { edit: false },
    },
  ],
} satisfies ContentProtectionPolicy

Prevents changing a heading's level attribute. Its text remains editable, and the heading itself can still be removed.

Keep headings while allowing text edits

const policy = {
  version: 1,
  rules: [
    {
      selector: {
        target: ['nodeType', 'attribute'],
        node: { types: ['heading'] },
        names: ['level'],
      },
      permissions: { edit: false },
    },
  ],
} satisfies ContentProtectionPolicy

Keeps headings in place and prevents level changes while allowing their text and formatting to change. Unlike target: 'node', this does not lock the entire heading.

const policy = {
  version: 1,
  rules: [
    {
      selector: {
        target: ['markType', 'markAttribute'],
        mark: {
          types: ['link'],
          within: { types: ['heading'] },
        },
        names: ['href'],
      },
      permissions: { edit: false },
    },
  ],
} satisfies ContentProtectionPolicy

Prevents adding or removing links in headings and changing their destinations. Their text and other attributes remain editable. Links outside headings are unaffected. The heading is an ancestor condition; this rule does not protect the heading itself.

See nested ancestor matching for a target array constrained by several ancestors.

Hide blockquotes

const policy = {
  version: 1,
  rules: [
    {
      selector: { target: 'node', node: { types: ['blockquote'] } },
      permissions: { read: false },
    },
  ],
} satisfies ContentProtectionPolicy

Replaces blockquotes with redacted content and prevents editing them. The original content remains in the client-side document.

Allow edits only inside blockquotes

const policy = {
  version: 1,
  rules: [
    {
      selector: { target: 'nodeContent', node: { types: ['doc'] } },
      permissions: { edit: false },
    },
    {
      priority: 10,
      selector: { target: 'nodeContent', node: { types: ['blockquote'] } },
      permissions: { edit: true },
    },
    {
      priority: 20,
      selector: { target: 'nodeType', node: { types: ['blockquote'] } },
      permissions: { edit: false },
    },
  ],
} satisfies ContentProtectionPolicy

Locks the document, then allows changes inside existing blockquotes. The final rule protects the blockquotes themselves, including nested ones; their attributes remain protected by the document-wide rule.

Protect nested content

const policy = {
  version: 1,
  rules: [
    {
      selector: { target: 'nodeContent', node: { types: ['doc'] } },
      permissions: { edit: false },
    },
    {
      priority: 10,
      selector: { target: 'nodeContent', node: { types: ['blockquote'] } },
      permissions: { edit: true },
    },
    {
      priority: 20,
      selector: {
        target: 'nodeContent',
        node: { types: ['blockquote'], within: { types: ['blockquote'] } },
      },
      permissions: { edit: false },
    },
  ],
} satisfies ContentProtectionPolicy

Allows editing inside outer blockquotes while protecting the contents of nested blockquotes at every depth. The nested deny wins because of its priority, not its selector depth. To protect the nested Nodes' type, existence and attributes too, use target: 'node' for the nested deny.

Protect table columns and headers

const policy = {
  version: 1,
  rules: [
    {
      selector: { target: 'children', node: { types: ['tableRow'] } },
      permissions: { edit: false },
    },
    {
      selector: { target: 'node', node: { types: ['tableHeader'] } },
      permissions: { edit: false },
    },
    {
      selector: {
        target: 'attribute',
        node: { types: ['tableCell', 'tableHeader'] },
        names: ['colspan', 'rowspan'],
      },
      permissions: { edit: false },
    },
  ],
} satisfies ContentProtectionPolicy

Protects the cell sequence in existing rows, header text and header cells. It also prevents changing cell spans. Body-cell content stays editable, and whole body rows can be added or removed.

See the policy language for selector types and rule precedence, or use the Slot filling helper for Slot-only editing.