plone-client 1.0.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. plone_client/__init__.py +51 -0
  2. plone_client/auth.py +120 -0
  3. plone_client/block_docs/LICENSE.md +21 -0
  4. plone_client/block_docs/description.md +13 -0
  5. plone_client/block_docs/detachedSlate.md +5 -0
  6. plone_client/block_docs/gridBlock.md +38 -0
  7. plone_client/block_docs/html.md +20 -0
  8. plone_client/block_docs/image.md +30 -0
  9. plone_client/block_docs/listing.md +45 -0
  10. plone_client/block_docs/maps.md +18 -0
  11. plone_client/block_docs/search.md +43 -0
  12. plone_client/block_docs/slate.md +81 -0
  13. plone_client/block_docs/teaser.md +25 -0
  14. plone_client/block_docs/title.md +13 -0
  15. plone_client/block_docs/toc.md +13 -0
  16. plone_client/block_docs/video.md +15 -0
  17. plone_client/blocks.py +280 -0
  18. plone_client/cli/__init__.py +48 -0
  19. plone_client/cli/app.py +119 -0
  20. plone_client/cli/content.py +490 -0
  21. plone_client/cli/mcp.py +103 -0
  22. plone_client/cli/operations.py +180 -0
  23. plone_client/cli/output.py +302 -0
  24. plone_client/cli/panels.py +60 -0
  25. plone_client/cli/profile.py +359 -0
  26. plone_client/cli/site.py +469 -0
  27. plone_client/cli/skill.py +64 -0
  28. plone_client/cli/state.py +111 -0
  29. plone_client/client.py +754 -0
  30. plone_client/credentials.py +150 -0
  31. plone_client/exceptions.py +189 -0
  32. plone_client/managers/__init__.py +54 -0
  33. plone_client/managers/base.py +37 -0
  34. plone_client/managers/block_types.py +296 -0
  35. plone_client/managers/content.py +315 -0
  36. plone_client/managers/discussion.py +231 -0
  37. plone_client/managers/files.py +289 -0
  38. plone_client/managers/navigation.py +73 -0
  39. plone_client/managers/search.py +230 -0
  40. plone_client/managers/site.py +337 -0
  41. plone_client/managers/types.py +178 -0
  42. plone_client/managers/users.py +263 -0
  43. plone_client/managers/workflow.py +117 -0
  44. plone_client/mcp_server.py +798 -0
  45. plone_client/operations.py +686 -0
  46. plone_client/profiles.py +593 -0
  47. plone_client/py.typed +0 -0
  48. plone_client/skill/SKILL.md +127 -0
  49. plone_client/utils.py +39 -0
  50. plone_client-1.0.0a1.dist-info/METADATA +127 -0
  51. plone_client-1.0.0a1.dist-info/RECORD +54 -0
  52. plone_client-1.0.0a1.dist-info/WHEEL +4 -0
  53. plone_client-1.0.0a1.dist-info/entry_points.txt +2 -0
  54. plone_client-1.0.0a1.dist-info/licenses/LICENSE.GPL +339 -0
@@ -0,0 +1,51 @@
1
+ """plone-client: a Python library and command line client for Plone sites.
2
+
3
+ Example::
4
+
5
+ from plone_client import PloneClient
6
+
7
+ with PloneClient.from_profile("blog") as site:
8
+ page = site.content.get("/about")
9
+ site.workflow.transition("/about", "publish")
10
+ """
11
+
12
+ from .auth import JWTAuth
13
+ from .auth import TokenAuth
14
+ from .client import PloneClient
15
+ from .exceptions import PloneAPIError
16
+ from .exceptions import PloneAuthenticationError
17
+ from .exceptions import PloneBadRequestError
18
+ from .exceptions import PloneClientError
19
+ from .exceptions import PloneConfigurationError
20
+ from .exceptions import PloneConflictError
21
+ from .exceptions import PloneConnectionError
22
+ from .exceptions import PloneForbiddenError
23
+ from .exceptions import PloneNotFoundError
24
+ from .exceptions import PloneOperationError
25
+ from .exceptions import PloneReadOnlyError
26
+ from .exceptions import PloneServerError
27
+ from .exceptions import PloneTimeoutError
28
+ from .managers.files import file_data
29
+
30
+
31
+ __version__ = "1.0.0a1"
32
+
33
+ __all__ = [
34
+ "JWTAuth",
35
+ "PloneAPIError",
36
+ "PloneAuthenticationError",
37
+ "PloneBadRequestError",
38
+ "PloneClient",
39
+ "PloneClientError",
40
+ "PloneConfigurationError",
41
+ "PloneConflictError",
42
+ "PloneConnectionError",
43
+ "PloneForbiddenError",
44
+ "PloneNotFoundError",
45
+ "PloneOperationError",
46
+ "PloneReadOnlyError",
47
+ "PloneServerError",
48
+ "PloneTimeoutError",
49
+ "TokenAuth",
50
+ "file_data",
51
+ ]
plone_client/auth.py ADDED
@@ -0,0 +1,120 @@
1
+ """Authentication strategies for :class:`~plone_client.PloneClient`.
2
+
3
+ Three ways to authenticate against ``plone.restapi`` are supported:
4
+
5
+ * HTTP basic authentication (:class:`httpx.BasicAuth`), the simplest option
6
+ and the default when a username and password are given;
7
+ * a JWT obtained from ``@login`` (:class:`JWTAuth`), which logs in again
8
+ when the token expires;
9
+ * a token issued beforehand (:class:`TokenAuth`).
10
+
11
+ Any other :class:`httpx.Auth` can be passed to the client as well.
12
+ """
13
+
14
+ from .exceptions import error_from_response
15
+ from collections.abc import Generator
16
+ from typing import Literal
17
+
18
+ import httpx
19
+
20
+
21
+ AuthMethod = Literal["basic", "jwt"]
22
+
23
+
24
+ class TokenAuth(httpx.Auth):
25
+ """Send a token issued beforehand as ``Authorization: Bearer``.
26
+
27
+ :param token: The JWT.
28
+ """
29
+
30
+ def __init__(self, token: str) -> None:
31
+ self.token = token
32
+
33
+ def auth_flow(
34
+ self, request: httpx.Request
35
+ ) -> Generator[httpx.Request, httpx.Response, None]:
36
+ request.headers["Authorization"] = f"Bearer {self.token}"
37
+ yield request
38
+
39
+
40
+ class JWTAuth(httpx.Auth):
41
+ """Log in with ``POST @login`` and send the JWT it returns.
42
+
43
+ The login happens on the first request. When the site answers ``401``,
44
+ the token is assumed to be expired: the flow logs in again and repeats
45
+ the request once.
46
+
47
+ :param username: Login name.
48
+ :param password: Password.
49
+ :param login_url: Absolute URL of the site's ``@login`` service.
50
+ """
51
+
52
+ requires_response_body = True
53
+
54
+ def __init__(self, username: str, password: str, login_url: str) -> None:
55
+ self.username = username
56
+ self.password = password
57
+ self.login_url = login_url
58
+ self.token = ""
59
+
60
+ def _login(self) -> Generator[httpx.Request, httpx.Response, None]:
61
+ """Ask the site for a new token.
62
+
63
+ :raises PloneAuthenticationError: If the site refuses the credentials.
64
+ """
65
+ response = yield httpx.Request(
66
+ "POST",
67
+ self.login_url,
68
+ json={"login": self.username, "password": self.password},
69
+ headers={"Accept": "application/json"},
70
+ )
71
+ if response.status_code != 200:
72
+ raise error_from_response(response)
73
+ self.token = response.json().get("token", "")
74
+
75
+ def auth_flow(
76
+ self, request: httpx.Request
77
+ ) -> Generator[httpx.Request, httpx.Response, None]:
78
+ if not self.token:
79
+ yield from self._login()
80
+ request.headers["Authorization"] = f"Bearer {self.token}"
81
+ response = yield request
82
+ if response.status_code == 401:
83
+ yield from self._login()
84
+ request.headers["Authorization"] = f"Bearer {self.token}"
85
+ yield request
86
+
87
+
88
+ def build_auth(
89
+ *,
90
+ username: str = "",
91
+ password: str = "",
92
+ token: str = "",
93
+ method: AuthMethod = "basic",
94
+ login_url: str = "",
95
+ ) -> httpx.Auth | None:
96
+ """Pick the authentication for a set of credentials.
97
+
98
+ A token wins over a username and password. Without any credential the
99
+ client is anonymous.
100
+
101
+ :param username: Login name.
102
+ :param password: Password.
103
+ :param token: JWT issued beforehand.
104
+ :param method: ``basic`` or ``jwt``, used with a username and password.
105
+ :param login_url: Absolute URL of ``@login``, needed by ``jwt``.
106
+ :returns: The authentication, or ``None`` for anonymous access.
107
+ :raises ValueError: If the method is unknown or only half of the
108
+ username/password pair is given.
109
+ """
110
+ if token:
111
+ return TokenAuth(token)
112
+ if not (username or password):
113
+ return None
114
+ if not (username and password):
115
+ raise ValueError("Both a username and a password are needed")
116
+ if method == "basic":
117
+ return httpx.BasicAuth(username, password)
118
+ if method == "jwt":
119
+ return JWTAuth(username, password, login_url)
120
+ raise ValueError(f"Unknown authentication method: {method!r}")
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017 Plone Foundation
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,13 @@
1
+ ---
2
+ block: description
3
+ ---
4
+
5
+ Shows the description (summary) of the content item.
6
+
7
+ The block stores no data of its own: the text it shows is the content item's `description` field, so change that field to change the description.
8
+
9
+ ### Example
10
+
11
+ ```json
12
+ { "@type": "description" }
13
+ ```
@@ -0,0 +1,5 @@
1
+ ---
2
+ block: detachedSlate
3
+ ---
4
+
5
+ A text block that is not split into new blocks when you press Enter, for use inside other blocks. Its data has the same format as the text block (`slate`).
@@ -0,0 +1,38 @@
1
+ ---
2
+ block: gridBlock
3
+ ---
4
+
5
+ Places blocks side by side in columns, for example several teasers in a row. It holds up to {{maxLength}} blocks.
6
+
7
+ The grid contains its own blocks, in the same format as a page: `blocks` maps block IDs to block data, and `blocks_layout.items` lists those IDs in display order. Every ID must appear in both. The allowed blocks are {{allowedBlocks}}.
8
+
9
+ The grid controls the layout of its blocks, so alignment and size settings of the blocks inside it are not used.
10
+
11
+ ### Fields
12
+
13
+ - `blocks`: object of child blocks keyed by a unique ID (normally a UUID).
14
+ - `blocks_layout`: `{ "items": [...] }` with the child IDs in order.
15
+
16
+ ### Example
17
+
18
+ ```json
19
+ {
20
+ "@type": "gridBlock",
21
+ "blocks": {
22
+ "3f1a6c2e-0d8b-4a8e-9c55-1b2f7e9d4a01": {
23
+ "@type": "teaser",
24
+ "href": [{ "@id": "/news/article-one", "title": "Article one" }]
25
+ },
26
+ "8b7d0e4f-5c2a-4e61-a3f9-6d0c2b8e1f72": {
27
+ "@type": "teaser",
28
+ "href": [{ "@id": "/news/article-two", "title": "Article two" }]
29
+ }
30
+ },
31
+ "blocks_layout": {
32
+ "items": [
33
+ "3f1a6c2e-0d8b-4a8e-9c55-1b2f7e9d4a01",
34
+ "8b7d0e4f-5c2a-4e61-a3f9-6d0c2b8e1f72"
35
+ ]
36
+ }
37
+ }
38
+ ```
@@ -0,0 +1,20 @@
1
+ ---
2
+ block: html
3
+ ---
4
+
5
+ Renders raw HTML markup as it is.
6
+
7
+ Only use it for trusted markup that can't be built with other blocks: the HTML is not sanitized.
8
+
9
+ ### Fields
10
+
11
+ - `html`: the HTML markup.
12
+
13
+ ### Example
14
+
15
+ ```json
16
+ {
17
+ "@type": "html",
18
+ "html": "<p>Custom <strong>HTML</strong> content.</p>"
19
+ }
20
+ ```
@@ -0,0 +1,30 @@
1
+ ---
2
+ block: image
3
+ ---
4
+
5
+ Shows a single image, either an Image content item from the site or an external image, with optional alt text, link, alignment and size.
6
+
7
+ For an image from the site, set `url` to the path of the Image content item (for example `/my-folder/my-image`) or to its full URL, and set `image_field` to `image`. Both forms are stored as a UID-based path. Don't use an `@@images` scale URL.
8
+
9
+ For an external image, set `url` to the full image URL and leave out `image_field`.
10
+
11
+ Inside a container block such as the grid, the container controls the layout, so `align` and `size` are not used there.
12
+
13
+ ### Fields
14
+
15
+ - `url`: path or URL of the image.
16
+ - `image_field`: the image field of the content item, normally `image`. Only for images from the site.
17
+ - `alt`: alternative text for screen readers.
18
+ - `href`: optional link target.
19
+ - `align`, `size`: alignment and size on the page.
20
+
21
+ ### Example
22
+
23
+ ```json
24
+ {
25
+ "@type": "image",
26
+ "url": "/images/team-photo",
27
+ "image_field": "image",
28
+ "alt": "The team at the 2025 conference"
29
+ }
30
+ ```
@@ -0,0 +1,45 @@
1
+ ---
2
+ block: listing
3
+ ---
4
+
5
+ Shows a list of content items that match a catalog query, such as the latest news or upcoming events. The list updates as content is added. To link to specific items chosen by hand, use teasers instead.
6
+
7
+ The query is stored in `querystring`. The criteria that a site supports depend on its configuration, so get the available indexes and operations from the `@querystring` endpoint of the Plone REST API.
8
+
9
+ ### Fields
10
+
11
+ - `querystring`: an object with:
12
+ - `query`: list of criteria, each `{ "i": index, "o": operation, "v": value }`. Operations are full names, such as `plone.app.querystring.operation.selection.any`.
13
+ - `sort_on`, `sort_order` (`ascending` or `descending`): optional sorting. For descending order, also set `sort_order_boolean` to `true`.
14
+ - `b_size` (items per page) and `limit` (maximum number of items): optional, given as strings, for example `"10"`.
15
+ - `variation`: how the items are shown. See the block's variations for the available ones.
16
+ - `headline`, `headlineTag`: optional heading above the list.
17
+
18
+ ### Example
19
+
20
+ ```json
21
+ {
22
+ "@type": "listing",
23
+ "headline": "Latest news",
24
+ "headlineTag": "h2",
25
+ "variation": "summary",
26
+ "querystring": {
27
+ "query": [
28
+ {
29
+ "i": "portal_type",
30
+ "o": "plone.app.querystring.operation.selection.any",
31
+ "v": ["News Item"]
32
+ },
33
+ {
34
+ "i": "path",
35
+ "o": "plone.app.querystring.operation.string.absolutePath",
36
+ "v": "/news"
37
+ }
38
+ ],
39
+ "sort_on": "effective",
40
+ "sort_order": "descending",
41
+ "sort_order_boolean": true,
42
+ "b_size": "6"
43
+ }
44
+ }
45
+ ```
@@ -0,0 +1,18 @@
1
+ ---
2
+ block: maps
3
+ ---
4
+
5
+ Embeds an interactive map from a map service.
6
+
7
+ ### Fields
8
+
9
+ - `url`: the embed URL of the map. For Google Maps, use the URL from *Share > Embed a map*, which starts with `https://www.google.com/maps/embed`.
10
+
11
+ ### Example
12
+
13
+ ```json
14
+ {
15
+ "@type": "maps",
16
+ "url": "https://www.google.com/maps/embed?pb=..."
17
+ }
18
+ ```
@@ -0,0 +1,43 @@
1
+ ---
2
+ block: search
3
+ ---
4
+
5
+ Shows a list of content items that visitors can search, sort and filter with facets. Unlike the listing block, results change as the visitor interacts with the block.
6
+
7
+ The base query in `query` has the same shape as the listing block's `querystring`: an object that contains a `query` list of criteria, not the list itself. It filters the results before the visitor's own search is applied. Use `{ "query": [] }` to start from all content. The criteria that a site supports depend on its configuration, so get the available indexes and operations from the `@querystring` endpoint of the Plone REST API.
8
+
9
+ ### Fields
10
+
11
+ - `query`: the base query, with `query` (list of `{ "i", "o", "v" }` criteria), and optional `sort_on`, `sort_order`, `b_size` and `limit`. `b_size` and `limit` are strings, for example `"10"`.
12
+ - `showSearchInput`, `showSortOn`, `showTotalResults`: booleans that show the search field, the sort menu and the result count.
13
+ - `facets`: list of facets, each `{ "title", "field", "type" }`, where `field` is a catalog index and `type` is one of the facet widgets configured for the block (Volto ships `selectFacet`, `checkboxFacet`, `daterangeFacet` and `toggleFacet`). Optional `hidden` and `advanced` booleans.
14
+ - `variation`: where the facets are placed. See the block's variations for the available ones.
15
+ - `headline`: optional heading.
16
+
17
+ ### Example
18
+
19
+ ```json
20
+ {
21
+ "@type": "search",
22
+ "headline": "News",
23
+ "query": {
24
+ "query": [
25
+ {
26
+ "i": "portal_type",
27
+ "o": "plone.app.querystring.operation.selection.any",
28
+ "v": ["News Item"]
29
+ }
30
+ ],
31
+ "sort_on": "effective",
32
+ "sort_order": "descending",
33
+ "b_size": "10"
34
+ },
35
+ "showSearchInput": true,
36
+ "showSortOn": true,
37
+ "showTotalResults": true,
38
+ "facets": [
39
+ { "title": "Topic", "field": "Subject", "type": "checkboxFacet" }
40
+ ],
41
+ "variation": "facetsRightSide"
42
+ }
43
+ ```
@@ -0,0 +1,81 @@
1
+ ---
2
+ block: slate
3
+ ---
4
+
5
+ Holds rich text: a paragraph, a heading, a list or a quote, with inline formatting and links. It is the most common block on a page.
6
+
7
+ Each text block normally holds one paragraph, heading, list or quote. For several paragraphs, add one text block per paragraph, as the editor does when you press Enter.
8
+
9
+ The text is stored twice:
10
+
11
+ - `value`: the rich text, as a list of Slate nodes.
12
+ - `plaintext`: the same text without formatting. It is what the site indexes for search and what decides whether the block is empty, so keep it in sync with `value`.
13
+
14
+ ### The `value` format
15
+
16
+ `value` is a list with one element (sometimes more). An element is `{ "type": ..., "children": [...] }`, and the innermost children are text nodes, `{ "text": "..." }`. Every element has at least one child, even if it's `{ "text": "" }`.
17
+
18
+ - Block-level types: `p` (paragraph), `h2`, `h3`, `h4` (headings; the page title is the `h1`), `ul` and `ol` (lists, whose children are `li` elements), and `blockquote`.
19
+ - Inline formatting is not a property of text nodes. It is an element that wraps them: `strong`, `em`, `u`, `del`, `sub`, `sup` and `code`.
20
+ - Links are `{ "type": "link", "data": { "url": ... }, "children": [...] }`. For content from the site, use its path, for example `/news/annual-report`; it is stored as a UID-based link. For an external link, use the full URL.
21
+
22
+ ### Fields
23
+
24
+ - `value`: the rich text, as described above.
25
+ - `plaintext`: the text of `value` without formatting.
26
+ - `override_toc`, `level`, `entry_text`: optional. Replace this block's entry in a table of contents block.
27
+
28
+ ### Examples
29
+
30
+ A heading:
31
+
32
+ ```json
33
+ {
34
+ "@type": "slate",
35
+ "value": [{ "type": "h2", "children": [{ "text": "Our services" }] }],
36
+ "plaintext": "Our services"
37
+ }
38
+ ```
39
+
40
+ A paragraph with bold text and a link:
41
+
42
+ ```json
43
+ {
44
+ "@type": "slate",
45
+ "value": [
46
+ {
47
+ "type": "p",
48
+ "children": [
49
+ { "text": "Read the " },
50
+ { "type": "strong", "children": [{ "text": "annual report" }] },
51
+ { "text": " on the " },
52
+ {
53
+ "type": "link",
54
+ "data": { "url": "/news/annual-report" },
55
+ "children": [{ "text": "news page" }]
56
+ },
57
+ { "text": "." }
58
+ ]
59
+ }
60
+ ],
61
+ "plaintext": "Read the annual report on the news page."
62
+ }
63
+ ```
64
+
65
+ A bulleted list:
66
+
67
+ ```json
68
+ {
69
+ "@type": "slate",
70
+ "value": [
71
+ {
72
+ "type": "ul",
73
+ "children": [
74
+ { "type": "li", "children": [{ "text": "First item" }] },
75
+ { "type": "li", "children": [{ "text": "Second item" }] }
76
+ ]
77
+ }
78
+ ],
79
+ "plaintext": "First item Second item"
80
+ }
81
+ ```
@@ -0,0 +1,25 @@
1
+ ---
2
+ block: teaser
3
+ ---
4
+
5
+ Shows a card that links to one content item, with its title, description and image. Use it for highlights chosen by hand. To show items that match a query, use a listing block instead.
6
+
7
+ Usually only `href` is needed: the teaser reads the title, description and image from the target item, and follows it when that item changes. Set `overwrite` to `true` to replace them with your own values; the teaser then no longer follows changes to the target.
8
+
9
+ ### Fields
10
+
11
+ - `href`: a list with one item, `[{ "@id": path or URL, "title": title }]`. Use a path for content from the site, or a full URL for an external link.
12
+ - `overwrite`: use the teaser's own `title`, `description` and `preview_image` instead of the target's.
13
+ - `preview_image`: only with `overwrite`. A list with one item, `[{ "@id": path, "image_field": "image" }]`; leave out `image_field` for an external image.
14
+ - `styles.align`: `left`, `right` or `center`. Not used inside a grid, where the grid controls the layout.
15
+
16
+ ### Example
17
+
18
+ ```json
19
+ {
20
+ "@type": "teaser",
21
+ "href": [
22
+ { "@id": "/news/annual-report-2025", "title": "Annual Report 2025" }
23
+ ]
24
+ }
25
+ ```
@@ -0,0 +1,13 @@
1
+ ---
2
+ block: title
3
+ ---
4
+
5
+ Shows the title of the content item. It is part of the default layout of new pages, and a page normally has exactly one.
6
+
7
+ The block stores no data of its own: the text it shows is the content item's `title` field, so change that field to change the title.
8
+
9
+ ### Example
10
+
11
+ ```json
12
+ { "@type": "title" }
13
+ ```
@@ -0,0 +1,13 @@
1
+ ---
2
+ block: toc
3
+ ---
4
+
5
+ Shows a table of contents built from the headings on the page, so readers can jump to a section.
6
+
7
+ It reads the headings of the page's text blocks when it renders, so it needs no content of its own. Its settings (title, heading levels, ordered list) are optional.
8
+
9
+ ### Example
10
+
11
+ ```json
12
+ { "@type": "toc" }
13
+ ```
@@ -0,0 +1,15 @@
1
+ ---
2
+ block: video
3
+ ---
4
+
5
+ Embeds a video from YouTube, Vimeo, PeerTube, or a direct link to a video file.
6
+
7
+ ### Fields
8
+
9
+ - `url`: the video URL as you would share it, for example a YouTube watch or share link.
10
+
11
+ ### Example
12
+
13
+ ```json
14
+ { "@type": "video", "url": "https://youtu.be/n837R4emhns" }
15
+ ```