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.
- plone_client/__init__.py +51 -0
- plone_client/auth.py +120 -0
- plone_client/block_docs/LICENSE.md +21 -0
- plone_client/block_docs/description.md +13 -0
- plone_client/block_docs/detachedSlate.md +5 -0
- plone_client/block_docs/gridBlock.md +38 -0
- plone_client/block_docs/html.md +20 -0
- plone_client/block_docs/image.md +30 -0
- plone_client/block_docs/listing.md +45 -0
- plone_client/block_docs/maps.md +18 -0
- plone_client/block_docs/search.md +43 -0
- plone_client/block_docs/slate.md +81 -0
- plone_client/block_docs/teaser.md +25 -0
- plone_client/block_docs/title.md +13 -0
- plone_client/block_docs/toc.md +13 -0
- plone_client/block_docs/video.md +15 -0
- plone_client/blocks.py +280 -0
- plone_client/cli/__init__.py +48 -0
- plone_client/cli/app.py +119 -0
- plone_client/cli/content.py +490 -0
- plone_client/cli/mcp.py +103 -0
- plone_client/cli/operations.py +180 -0
- plone_client/cli/output.py +302 -0
- plone_client/cli/panels.py +60 -0
- plone_client/cli/profile.py +359 -0
- plone_client/cli/site.py +469 -0
- plone_client/cli/skill.py +64 -0
- plone_client/cli/state.py +111 -0
- plone_client/client.py +754 -0
- plone_client/credentials.py +150 -0
- plone_client/exceptions.py +189 -0
- plone_client/managers/__init__.py +54 -0
- plone_client/managers/base.py +37 -0
- plone_client/managers/block_types.py +296 -0
- plone_client/managers/content.py +315 -0
- plone_client/managers/discussion.py +231 -0
- plone_client/managers/files.py +289 -0
- plone_client/managers/navigation.py +73 -0
- plone_client/managers/search.py +230 -0
- plone_client/managers/site.py +337 -0
- plone_client/managers/types.py +178 -0
- plone_client/managers/users.py +263 -0
- plone_client/managers/workflow.py +117 -0
- plone_client/mcp_server.py +798 -0
- plone_client/operations.py +686 -0
- plone_client/profiles.py +593 -0
- plone_client/py.typed +0 -0
- plone_client/skill/SKILL.md +127 -0
- plone_client/utils.py +39 -0
- plone_client-1.0.0a1.dist-info/METADATA +127 -0
- plone_client-1.0.0a1.dist-info/RECORD +54 -0
- plone_client-1.0.0a1.dist-info/WHEEL +4 -0
- plone_client-1.0.0a1.dist-info/entry_points.txt +2 -0
- plone_client-1.0.0a1.dist-info/licenses/LICENSE.GPL +339 -0
plone_client/__init__.py
ADDED
|
@@ -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,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
|
+
```
|