WP Annotation Comments

Front and back-end annotations implemented as custom comment types. Conforms to the W3C annotation data model for annotation selectors and maintains some parity with the W3C annotation object model and protocol — while still doing things the WordPress way, which maximizes compatibility with WP REST API utilities, including those in JavaScript.
Pull Request

https://github.com/WordPress/gutenberg/pull/4386
This is an alternative to annotations as a custom post type in #4385

Create Annotation
POST /wp-json/wp/v2/annotations

Create a new annotation.

Note: While the API supports both front and back-end annotations, as a security precaution, front-end annotations are disabled for now. At this time, the main focus is on back-end annotations only.

Request parameters

password
string optional

Post password (if password-protected and authenticated user is unable to edit it, but is allowed to annotate; e.g., edge case in front-end annotations). Defaults to current password cookie.

Default:
[cookie value]

Request body

Object
properties
post
integer required

Post ID.

type
One of

An annotation comment type.

Default:
annotation
string
Enumeration:
annotation

Front-end annotation (default).

admin_annotation

Back-end annotation.

parent
integer

Parent ID, if it’s a reply.

Default:
0
author
integer

Author’s user ID. Requires ability to edit_others_annotations. Default is that of the authenticated user. Default is 0 for front-end anonymous annotations; i.e., if those are allowed by the rest_allow_anonymous_comments filter.

Example:
1
author_name
string

Author’s name. Default is that of the authenticated user.

Example:
John Smith
author_email
string

Author’s email address. Default is that of the authenticated user.

Example:
john@example.com
author_url
string

Author’s URL. Default is that of the authenticated user.

Example:
https://john.example.com/
author_ip
string

Author’s IP address. Requires ability to edit_others_annotations. Default value is auto-detected using $_SERVER['REMOTE_ADDR'].

Example:
127.0..0.1
author_user_agent
string

Author’s user-agent (browser). Requires ability to edit_others_annotations. Default value is auto-detected using $_SERVER['HTTP_USER_AGENT'].

Example:
Mozilla/5.0
content
string required

Comment content. Back-end annotations are not spam or flood checked. Front-end annotations follow the same rules as other comment types. Both front and back-end annotations have a maximum length.

Min length: 1
Example:
Hello world!
via
string
Max length: 250
Pattern: [a-zA-Z0-9:_-]+
Example:
gutenberg
selector
Object

W3C annotation selector. Max JSON-encoded size is 16kb. SVG selectors allow up to 128kbs, but those are currently disabled, pending a security review. See also: https://jsfiddle.net/jaswrks/jpcL1t8a/

Examples:
{
    "type": "CssSelector",
    "value": "#foo > .bar"
}
{
    "type": "CssSelector",
    "value": "#foo > ul:nth-child(2) > li:nth-child(3)",
    "refinedBy": {
          "type": "TextPositionSelector",
          "start": 412,
          "end": 667
    }
}
{
    "type": "RangeSelector",
    "startSelector": {
          "type": "CssSelector",
          "value": "#foo > ul:nth-child(2) > li:nth-child(2)"
    },
    "endSelector": {
          "type": "CssSelector",
          "value": "#foo > ul:nth-child(2) > li:nth-child(8)"
    }
}
type
string

Type of selector.

Enumeration:
FragmentSelector
CssSelector
XPathSelector

Can select text nodes!

TextQuoteSelector
TextPositionSelector
DataPositionSelector
SvgSelector

Pending a security review.

RangeSelector
value
string

Required for CssSelector type. Other properties apply to other types.

status
string

The default status for a new back-end annotation is approve. The default status for a new front-end annotation is determined by wp_allow_comment(). To create an annotation with a restricted status (hold, 0, spam, trash) you must be able to edit_others_annotations.

Enumeration:
hold

Held for moderation. Same as ‘0’.

approved

Same as ‘1’ and ‘approve’.

approve

Same as ‘1’ and ‘approved’.

resolve

Archived as resolved.

reject

Archived as rejected.

archive

Archived for another reason.

spam

Front-end annotation spam.

trash

In the trash.

meta
Object

Any comment meta properties registered using register_meta(). Requires an authenticated user that can edit_annotations.

Example:
{
    "my_key": "my value",
    "another_key": {
        "another": "value"
    }
}
pattern properties
[a-z0-9_-]+
Any of

Any value.

string
number
Array of unknown
Object
additional properties
Object
Examples

i.e., Also supports all the same arguments as the REST API for comments.

Example 1
Example 2
Example 3
Example 4
{
    "post": 1,
    "type": "admin_annotation",
    "content": "Hello world!",
    "via": "gutenberg"
}
{
    "post": 1,
    "type": "admin_annotation",
    "content": "Hello world!",
    "via": "gutenberg",
    "selector": {
        "type": "CssSelector",
        "value": "#foo > .bar"
    }
}
{
    "post": 1,
    "type": "admin_annotation",
    "author": 1,
    "author_name": "John Smith",
    "author_email": "john@example.com",
    "author_url": "https://john.example.com/",
    "author_ip": "127.0..0.1",
    "author_user_agent": "Mozilla/5.0",
    "content": "Hello world!",
    "via": "gutenberg",
    "selector": {
        "type": "CSSSelector",
        "value": "#foo > ul:nth-child(2) > li:nth-child(3)",
        "refinedBy": {
              "type": "TextPositionSelector",
              "start": 412,
              "end": 667
        }
    }
}
{
    "post": 1,
    "type": "admin_annotation",
    "author": 1,
    "author_name": "John Smith",
    "author_email": "john@example.com",
    "author_url": "https://john.example.com/",
    "author_ip": "127.0..0.1",
    "author_user_agent": "Mozilla/5.0",
    "content": "Hello world!",
    "via": "gutenberg",
    "selector": {
        "type": "RangeSelector",
        "startSelector": {
              "type": "CssSelector",
              "value": "#foo > ul:nth-child(2) > li:nth-child(2)"
        },
        "endSelector": {
              "type": "CssSelector",
              "value": "#foo > ul:nth-child(2) > li:nth-child(8)"
        }
    },
    "meta": {
        "my_key": "my value",
        "another_key": {
            "another": "value"
        }
    }
}
Get Annotation
GET /wp-json/wp/v2/annotations/{id}

Get a single annotation by ID.

Path variables

id
integer required

Annotation ID.

Request parameters

context
string optional

REST API context.

Enumeration:
view
edit
embed
Default:
view
password
string optional

Post password (if protected and the authenticated user is unable to edit it, but is allowed to view). Defaults to current password cookie.

Default:
[cookie value]
Example 1
Example 2

?context=view

{
    "id": 1,
    "post": 1,
    "parent": 0,
    "author": 1,
    "author_name": "John",
    "author_url": "https://example.john.com/",
    "date": "2018-01-11T11:09:03",
    "date_gmt": "2018-01-11T11:09:03",
    "content": {
        "rendered": "<p>Hello world!</p>\n"
    },
    "link": "https://example.com/hello-world/#comment-1",
    "status": "approved",
    "type": "admin_annotation",
    "author_avatar_urls": {
        "24": "https://secure.gravatar.com/avatar/e50c12f18247416a1f46f102c5c826c2?s=24&d=mm&r=g",
        "48": "https://secure.gravatar.com/avatar/e50c12f18247416a1f46f102c5c826c2?s=48&d=mm&r=g",
        "96": "https://secure.gravatar.com/avatar/e50c12f18247416a1f46f102c5c826c2?s=96&d=mm&r=g"
    },
    "meta": [],
    "via": "gutenberg",
    "selector": {
        "type": "CssSelector",
        "value": "#foo > .bar"
    },
    "_links": {
        "self": [
            {
                "href": "https://example.com/wp-json/wp/v2/annotations/1"
            }
        ],
        "collection": [
            {
                "href": "https://example.com/wp-json/wp/v2/annotations"
            }
        ],
        "author": [
            {
                "embeddable": true,
                "href": "https://example.com/wp-json/wp/v2/users/1"
            }
        ],
        "up": [
            {
                "embeddable": true,
                "post_type": "post",
                "href": "https://example.com/wp-json/wp/v2/posts/1"
            }
        ]
    }
}

?context=edit

{
    "id": 1,
    "post": 1,
    "parent": 0,
    "author": 1,
    "author_name": "John",
    "author_email": "john@example.com",
    "author_url": "https://example.john.com/",
    "author_ip": "192.168.42.1",
    "author_user_agent": "Mozilla/5.0",
    "date": "2018-01-11T11:09:03",
    "date_gmt": "2018-01-11T11:09:03",
    "content": {
        "rendered": "<p>Hello world!</p>\n",
        "raw": "Hello world!"
    },
    "link": "https://example.com/hello-world/#comment-1",
    "status": "approved",
    "type": "admin_annotation",
    "author_avatar_urls": {
        "24": "https://secure.gravatar.com/avatar/e50c12f18247416a1f46f102c5c826c2?s=24&d=mm&r=g",
        "48": "https://secure.gravatar.com/avatar/e50c12f18247416a1f46f102c5c826c2?s=48&d=mm&r=g",
        "96": "https://secure.gravatar.com/avatar/e50c12f18247416a1f46f102c5c826c2?s=96&d=mm&r=g"
    },
    "meta": [],
    "via": "gutenberg",
    "selector": {
        "type": "CssSelector",
        "value": "#foo > .bar"
    },
    "_links": {
        "self": [
            {
                "href": "https://example.com/wp-json/wp/v2/annotations/1"
            }
        ],
        "collection": [
            {
                "href": "https://example.com/wp-json/wp/v2/annotations"
            }
        ],
        "author": [
            {
                "embeddable": true,
                "href": "https://example.com/wp-json/wp/v2/users/1"
            }
        ],
        "up": [
            {
                "embeddable": true,
                "post_type": "post",
                "href": "https://example.com/wp-json/wp/v2/posts/1"
            }
        ]
    }
}
Update Annotation
PUT /wp-json/wp/v2/annotations/{id}

Update annotation properties.

Path variables

id
integer required

Annotation ID.

Request parameters

password
string optional

Post password (if password-protected and authenticated user is unable to edit it, but is allowed to edit annotations; e.g., edge case in front-end annotations). Defaults to current password cookie.

Default:
[cookie value]

Request body

Object
properties
author
integer

Author’s user ID. Requires ability to edit_others_annotations.

Example:
1
author_name
string

Author’s name. Requires ability to edit_others_annotations.

Example:
John Smith
author_email
string

Author’s email address. Requires ability to edit_others_annotations.

Example:
john@example.com
author_url
string

Author’s URL. Requires ability to edit_others_annotations.

Example:
https://john.example.com/
author_ip
string

Author’s IP address. Requires ability to edit_others_annotations.

Example:
127.0..0.1
author_user_agent
string

Author’s user-agent (browser). Requires ability to edit_others_annotations.

Example:
Mozilla/5.0
content
string

Comment content. Back-end annotations are not spam or flood checked. Front-end annotations follow the same rules as other comment types. Both front and back-end annotations have a maximum length.

Min length: 1
Example:
Hello world!
via
string
Max length: 250
Pattern: [a-zA-Z0-9:_-]+
Example:
gutenberg
selector
Object

W3C annotation selector. Max JSON-encoded size is 16kb. SVG selectors allow up to 128kbs, but those are currently disabled, pending a security review. See also: https://jsfiddle.net/jaswrks/jpcL1t8a/

Examples:
{
    "type": "CssSelector",
    "value": "#foo > .bar"
}
{
    "type": "CssSelector",
    "value": "#foo > ul:nth-child(2) > li:nth-child(3)",
    "refinedBy": {
          "type": "TextPositionSelector",
          "start": 412,
          "end": 667
    }
}
{
    "type": "RangeSelector",
    "startSelector": {
          "type": "CssSelector",
          "value": "#foo > ul:nth-child(2) > li:nth-child(2)"
    },
    "endSelector": {
          "type": "CssSelector",
          "value": "#foo > ul:nth-child(2) > li:nth-child(8)"
    }
}
type
string

Type of selector.

Enumeration:
FragmentSelector
CssSelector
XPathSelector

Can select text nodes!

TextQuoteSelector
TextPositionSelector
DataPositionSelector
SvgSelector

Pending a security review.

RangeSelector
value
string

Required for CssSelector type. Other properties apply to other types.

status
string

To set a restricted status (hold, 0, spam, trash) you must be able to edit_others_annotations. However, if the authenticated user is the annotation author, and they can delete_annotation, trash becomes available.

Enumeration:
hold

Held for moderation. Same as ‘0’.

approved

Same as ‘1’ and ‘approve’.

approve

Same as ‘1’ and ‘approved’.

resolve

Archived as resolved.

reject

Archived as rejected.

archive

Archived for another reason.

spam

Front-end annotation spam.

trash

In the trash.

meta
Object

Any comment meta properties registered using register_meta().

Example:
{
    "my_key": "my value",
    "another_key": {
        "another": "value"
    }
}
pattern properties
[a-z0-9_-]+
Any of

Any value.

string
number
Array of unknown
Object
additional properties
Object
Examples

i.e., Also supports all the same arguments as the REST API for comments, except that post, type, and parent are currently readonly in annotation comment types.

Example 1
Example 2
Example 3
Example 4
Example 5
{
    "content": "Hello world!"
}
{
    "status": "resolve"
}
{
    "status": "reject"
}
{
    "status": "archive"
}
{
    "author": 1,
    "author_name": "John Smith",
    "author_email": "john@example.com",
    "author_url": "https://john.example.com/",
    "author_ip": "127.0..0.1",
    "author_user_agent": "Mozilla/5.0",
    "content": "Hello world!",
    "status": "approved",
    "via": "gutenberg",
    "selector": {
        "type": "CSSSelector",
        "value": "#foo > ul:nth-child(2) > li:nth-child(3)",
        "refinedBy": {
              "type": "TextPositionSelector",
              "start": 412,
              "end": 667
        }
    },
    "meta": {
        "my_key": "my value",
        "another_key": {
            "another": "value"
        }
    }
}
Delete Annotation
DELETE /wp-json/wp/v2/annotations/{id}

Delete a single annotation by ID.

Path variables

id
integer required

Annotation ID.

Request parameters

force
integer optional

Bypass trash and force deletion?

Enumeration:
0

Trash (default).

1

Permanently delete.

Default:
0
password
string optional

Post password (if protected and the authenticated user is unable to edit it, but is allowed to delete — edge case).

Default:
[cookie value]
Query Annotations
GET /wp-json/wp/v2/annotations

Also supports all the same arguments as the REST API for comments; e.g., pagination, order.

Request parameters

context
string optional

REST API context.

Enumeration:
view
edit
embed
Default:
view
post
string required

Post ID. Array or comma-delimited string.

Example:
1,2,3
type
string required

Annotation comment type. Array or comma-delimited string.

Example:
annotation,admin_annotation
password
string optional

Password for a single post (if protected and the authenticated user is unable to edit it, but is allowed to view). @TODO Support multiple passwords indexed by order of post given in query.

parent
string optional

Parent annotation ID. Array or comma-delimited string. If hierarchical is given, parent defaults to 0 unless defined explicitly. This way hiearchical queries start from top-level annotations by default.

Example:
4,5,6
author
string optional

Author ID. Requires ability to edit_others_annotations. However, any authenticated user can set this to their own user ID, which allows them to query their own annotations.

status
string optional

Status. Array or comma-delimited string. To query a restricted status (hold, 0, spam, trash) you must be able to edit_others_annotations. However, if author is given, and the authenticated user is that author, and they can delete_annotations, trash becomes available.

Default:
approve
Example:
approve,resolve,reject,archive
via
string optional

Annotation client identifier. Array or comma-delimited string.

Example:
gutenberg,other
hierarchical
string optional

Hierarchical response format?

Enumeration:
flat

All children (deeply) are simply appended.

threaded

Each annotation includes a children property containing an array of any replies.

Example 1
Example 2

?context=view&hierarchical=threaded

[
    {
        "id": 1,
        "post": 1,
        "parent": 0,
        "author": 1,
        "author_name": "John",
        "author_url": "https://john.example.com",
        "date": "2018-01-11T12:21:10",
        "date_gmt": "2018-01-11T12:21:10",
        "content": {
            "rendered": "<p>Hello world!</p>\n"
        },
        "link": "https://example.com/hello-world/#comment-1",
        "status": "approved",
        "type": "admin_annotation",
        "author_avatar_urls": {
            "24": "https://secure.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=24&d=mm&r=g",
            "48": "https://secure.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=48&d=mm&r=g",
            "96": "https://secure.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=96&d=mm&r=g"
        },
        "meta": [],
        "via": "gutenberg",
        "selector": {
            "type": "CssSelector",
            "value": "#foo"
        },
        "_links": {
            "self": [
                {
                    "href": "https://example.com/wp-json/wp/v2/annotations/1"
                }
            ],
            "collection": [
                {
                    "href": "https://example.com/wp-json/wp/v2/annotations"
                }
            ],
            "author": [
                {
                    "embeddable": true,
                    "href": "https://example.com/wp-json/wp/v2/users/1"
                }
            ],
            "up": [
                {
                    "embeddable": true,
                    "post_type": "post",
                    "href": "https://example.com/wp-json/wp/v2/posts/1"
                }
            ]
        },
        "children": [
            {
                "id": 2,
                "post": 1,
                "parent": 1,
                "author": 2,
                "author_name": "Mary",
                "author_url": "https://mary.example.com",
                "date": "2018-01-11T12:21:10",
                "date_gmt": "2018-01-11T12:21:10",
                "content": {
                    "rendered": "<p>Hello world!</p>\n"
                },
                "link": "https://example.com/hello-world/#comment-2",
                "status": "approved",
                "type": "admin_annotation",
                "author_avatar_urls": {
                    "24": "https://secure.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=24&d=mm&r=g",
                    "48": "https://secure.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=48&d=mm&r=g",
                    "96": "https://secure.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=96&d=mm&r=g"
                },
                "meta": [],
                "via": "gutenberg",
                "selector": null,
                "_links": {
                    "self": [
                        {
                            "href": "https://example.com/wp-json/wp/v2/annotations/2"
                        }
                    ],
                    "collection": [
                        {
                            "href": "https://example.com/wp-json/wp/v2/annotations"
                        }
                    ],
                    "author": [
                        {
                            "embeddable": true,
                            "href": "https://example.com/wp-json/wp/v2/users/2"
                        }
                    ],
                    "up": [
                        {
                            "embeddable": true,
                            "post_type": "post",
                            "href": "https://example.com/wp-json/wp/v2/posts/1"
                        }
                    ]
                },
                "children": []
            }
        ]
    }
]

?context=edit&hierarchical=flat

[
    {
        "id": 1,
        "post": 1,
        "parent": 0,
        "author": 1,
        "author_name": "John",
        "author_email": "john@example.com",
        "author_url": "https://john.example.com",
        "author_ip": "192.168.42.1",
        "author_user_agent": "Mozilla/5.0",
        "date": "2018-01-11T12:21:10",
        "date_gmt": "2018-01-11T12:21:10",
        "content": {
            "rendered": "<p>Hello world!</p>\n",
            "raw": "Hello world!"
        },
        "link": "https://wp.vm/hello-world/#comment-1",
        "status": "approved",
        "type": "admin_annotation",
        "author_avatar_urls": {
            "24": "https://secure.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=24&d=mm&r=g",
            "48": "https://secure.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=48&d=mm&r=g",
            "96": "https://secure.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=96&d=mm&r=g"
        },
        "meta": [],
        "via": "gutenberg",
        "selector": {
            "type": "CssSelector",
            "value": "#foo"
        },
        "_links": {
            "self": [
                {
                    "href": "https://wp.vm/wp-json/wp/v2/annotations/1"
                }
            ],
            "collection": [
                {
                    "href": "https://wp.vm/wp-json/wp/v2/annotations"
                }
            ],
            "author": [
                {
                    "embeddable": true,
                    "href": "https://wp.vm/wp-json/wp/v2/users/1"
                }
            ],
            "up": [
                {
                    "embeddable": true,
                    "post_type": "post",
                    "href": "https://wp.vm/wp-json/wp/v2/posts/1"
                }
            ]
        }
    },
    {
        "id": 2,
        "post": 1,
        "parent": 1,
        "author": 2,
        "author_name": "Mary",
        "author_email": "mary@example.com",
        "author_url": "https://mary.example.com",
        "author_ip": "192.168.42.1",
        "author_user_agent": "Mozilla/5.0",
        "date": "2018-01-11T12:21:10",
        "date_gmt": "2018-01-11T12:21:10",
        "content": {
            "rendered": "<p>Hello world!</p>\n",
            "raw": "Hello world!"
        },
        "link": "https://wp.vm/hello-world/#comment-2",
        "status": "approved",
        "type": "admin_annotation",
        "author_avatar_urls": {
            "24": "https://secure.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=24&d=mm&r=g",
            "48": "https://secure.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=48&d=mm&r=g",
            "96": "https://secure.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=96&d=mm&r=g"
        },
        "meta": [],
        "via": "gutenberg",
        "selector": null,
        "_links": {
            "self": [
                {
                    "href": "https://wp.vm/wp-json/wp/v2/annotations/2"
                }
            ],
            "collection": [
                {
                    "href": "https://wp.vm/wp-json/wp/v2/annotations"
                }
            ],
            "author": [
                {
                    "embeddable": true,
                    "href": "https://wp.vm/wp-json/wp/v2/users/2"
                }
            ],
            "up": [
                {
                    "embeddable": true,
                    "post_type": "post",
                    "href": "https://wp.vm/wp-json/wp/v2/posts/1"
                }
            ]
        }
    }
]
Capabilities

Annotation permissions sometimes resemble those associated with other comment types, but at other times (e.g., back-end annotations) they are not like other comments. So these additional caps offer a standard method by which to check annotation permissions.

Meta-caps check specific annotations by ID.

current_user_can( 'edit_annotation', 53254 )
  • read_annotation
  • edit_annotation
  • delete_annotation

This custom meta-cap checks a post by ID and the would-be comment type.

current_user_can( 'create_annotation', 123, 'admin_annotation' )
  • create_annotation

General pseudo-caps that optionally support args: post ID, comment type.

current_user_can( 'read_annotations', 123, 'admin_annotation' )
current_user_can( 'read_annotations' )
  • read_annotations

General pseudo-caps that optionally support arg: comment type.

current_user_can( 'edit_others_annotations', 'admin_annotation' )
current_user_can( 'edit_others_annotations' )
  • create_annotations
  • delete_annotations
  • delete_others_annotations
  • delete_private_annotations
  • delete_published_annotations
  • edit_annotations
  • edit_others_annotations
  • edit_private_annotations
  • edit_published_annotations
  • publish_annotations
  • read_private_annotations