diff --git a/docs/backend/annotations.md b/docs/backend/annotations.md
index 7371002fd..cb8e6ea62 100644
--- a/docs/backend/annotations.md
+++ b/docs/backend/annotations.md
@@ -1,13 +1,455 @@
---
myst:
html_meta:
- "description": "Annotations store arbitrary values on Python objects—such as a Plone site or HTTP request—for storage and caching purposes."
- "property=og:description": "Annotations store arbitrary values on Python objects—such as a Plone site or HTTP request—for storage and caching purposes."
+ "description": "How to use annotations to store arbitrary values on Python objects in Plone"
+ "property=og:description": "How to use annotations to store arbitrary values on Python objects in Plone"
"property=og:title": "Annotations"
- "keywords": "Annotations, plone.memoize, cache, portal, content object"
+ "keywords": "Plone, annotations, zope.annotation, storage, caching"
---
(backend-annotations-label)=
# Annotations
+Annotations provide a conflict-free way to attach arbitrary attributes to Python objects in Plone.
+They are used for storage and caching purposes throughout the system.
+
+
+## Introduction
+
+The annotation pattern allows you to store additional data on objects without modifying their class definitions.
+This is particularly useful when you need to extend existing content types with custom data or settings.
+
+Plone uses annotations for:
+
+- Storing behavior data from `plone.behavior` package
+- Caching values on HTTP request objects (`plone.memoize` cache decorators)
+- Storing settings information on the portal or content objects (various add-on products)
+- Assigned portlets and their settings
+
+The [`zope.annotation`](https://pypi.org/project/zope.annotation/) package provides the core implementation.
+
+
+## HTTP request example
+
+Store cached values on an HTTP request during the lifecycle of a single request.
+This allows you to cache computed values when the same computation function might be called from different, unrelated code paths.
+
+```python
+from zope.annotation.interfaces import IAnnotations
+
+# Use a non-conflicting key based on your package name
+KEY = "mypackage.something"
+
+annotations = IAnnotations(request)
+
+value = annotations.get(KEY, None)
+if value is None:
+ # Compute value and store it on request object for further look-ups
+ value = annotations[KEY] = something()
+```
+
+You can also use the global request from `zope.globalrequest`:
+
+```python
+from zope.annotation.interfaces import IAnnotations
+from zope.globalrequest import getRequest
+
+
+def get_cached_data():
+ """Get data, using request annotation as cache."""
+ request = getRequest()
+ KEY = "mypackage.cached_data"
+
+ annotations = IAnnotations(request)
+ data = annotations.get(KEY, None)
+ if data is None:
+ data = annotations[KEY] = expensive_computation()
+ return data
+```
+
+
+## Content annotations
+
+### Overview and basic usage
+
+Annotations are the recommended way to extend Plone content with custom settings.
+
+- Your add-on can store its settings on the Plone site root object using local utilities or annotations.
+- You can store custom settings on individual content objects using annotations.
+
+By default, annotations store:
+
+- Behavior data from `plone.behavior` package
+- Assigned portlets and their settings
+
+Example of storing a simple value:
+
+```python
+from zope.annotation.interfaces import IAnnotations
+
+# Assume context variable refers to some content item
+
+# Use a non-conflicting key based on your company and package name
+KEY = "yourcompany.packagename.magicalcontentnavigationsetting"
+
+annotations = IAnnotations(context)
+
+# Store a setting on the content item
+annotations[KEY] = True
+
+# Retrieve the setting
+value = annotations.get(KEY, False)
+```
+
+
+### Advanced content annotation
+
+The above example works for storing simple values.
+For more complex data, you can create custom annotation classes.
+This example shows how to add a simple "Like / Dislike" counter to a content object.
+
+```python
+class LikeDislike:
+ """Track likes and dislikes for a content item."""
+
+ def __init__(self):
+ self.reset()
+
+ def reset(self):
+ self._likes = set()
+ self._dislikes = set()
+
+ def liked_by(self, user_id):
+ """Record that a user liked this item."""
+ self._dislikes.discard(user_id)
+ self._likes.add(user_id)
+
+ def disliked_by(self, user_id):
+ """Record that a user disliked this item."""
+ self._likes.discard(user_id)
+ self._dislikes.add(user_id)
+
+ def status(self):
+ """Return the count of likes and dislikes."""
+ return len(self._likes), len(self._dislikes)
+```
+
+```{important}
+Ensure your custom annotation class can be [pickled](https://docs.python.org/3/library/pickle.html#what-can-be-pickled-and-unpickled).
+In Zope, this means you cannot hold references to content objects directly in your annotation.
+Use the UID of a content object if you need to keep a reference.
+```
+
+The recommended pattern to get (and create if not existing) your annotation:
+
+```python
+from zope.annotation.interfaces import IAnnotations
+from zope.annotation.interfaces import IAttributeAnnotatable
+
+KEY = "content.like.dislike" # Best practice: define in a config module
+
+
+def get_likes_dislikes_for(item):
+ """Factory for LikeDislike as annotation of content.
+
+ Args:
+ item: Any annotatable object (any Plone content)
+
+ Returns:
+ LikeDislike instance for this item
+ """
+ # Ensure the item is annotatable
+ if not IAttributeAnnotatable.providedBy(item):
+ raise TypeError("Item must be annotatable")
+
+ annotations = IAnnotations(item)
+ return annotations.setdefault(KEY, LikeDislike())
+```
+
+This pattern ensures that:
+
+- You won't create annotations on objects that can't support them.
+- A new annotation is created for your context object if it doesn't already exist.
+- You can work with your `LikeDislike` annotation object like any Python object.
+ All attribute changes will be stored automatically in the annotations.
+
+
+### Wrapping your annotation with an adapter
+
+The `zope.annotation` package provides a `factory()` function that transforms an annotation class into an adapter.
+Annotations created this way have location awareness, with `__parent__` and `__name__` attributes.
+
+Here's the improved example using `zope.annotation.factory()`:
+
+```python
+from zope.annotation import factory
+from zope.annotation.interfaces import IAnnotations
+from zope.component import adapter
+from zope.interface import Interface
+from zope.interface import implementer
+
+KEY = "content.like.dislike"
+
+
+class ILikeDislike(Interface):
+ """Interface for like/dislike annotation."""
+
+ def reset():
+ """Reinitialize counters."""
+
+ def liked_by(user_id):
+ """Record that a user liked this item."""
+
+ def disliked_by(user_id):
+ """Record that a user disliked this item."""
+
+ def status():
+ """Return tuple of (likes_count, dislikes_count)."""
+
+
+@implementer(ILikeDislike)
+@adapter(Interface) # Adapts any content; use a specific interface for targeted behavior
+class LikeDislike:
+ """Track likes and dislikes for content items."""
+
+ def __init__(self):
+ # Unlike regular adapters, the constructor takes no arguments.
+ # Access the annotated object through self.__parent__
+ self.reset()
+
+ def reset(self):
+ self._likes = set()
+ self._dislikes = set()
+
+ def liked_by(self, user_id):
+ self._dislikes.discard(user_id)
+ self._likes.add(user_id)
+
+ def disliked_by(self, user_id):
+ self._likes.discard(user_id)
+ self._dislikes.add(user_id)
+
+ def status(self):
+ return len(self._likes), len(self._dislikes)
+
+
+# Create the adapter factory
+LikeDislikeFactory = factory(LikeDislike, key=KEY)
+```
+
+Register the adapter in ZCML:
+
+```xml
+
+```
+
+Or register programmatically:
+
+```python
+from zope.component import provideAdapter
+
+provideAdapter(LikeDislikeFactory)
+```
+
+Usage:
+
+```python
+# Get a content item
+item = portal["my-document"]
+
+# Get its annotation via the adapter
+like_dislike = ILikeDislike(item)
+
+# Use the annotation
+like_dislike.liked_by("joe")
+like_dislike.disliked_by("jane")
+
+assert like_dislike.status() == (1, 1)
+assert like_dislike.__parent__ is item
+assert like_dislike.__name__ == KEY
+```
+
+```{tip}
+Read the full documentation in the `README.txt` file in the `zope.annotation` package for more advanced usages.
+```
+
+
+### Using annotations in behaviors
+
+The most common use of annotations in Plone 6 is through behaviors.
+The `plone.behavior` package uses `AnnotationStorage` to store behavior data.
+
+Example behavior using annotation storage:
+
+```python
+from plone.autoform.interfaces import IFormFieldProvider
+from plone.behavior import AnnotationStorage
+from plone.supermodel import model
+from zope import schema
+from zope.interface import provider
+
+
+@provider(IFormFieldProvider)
+class IReviewers(model.Schema):
+ """Behavior to assign reviewers to content."""
+
+ official_reviewers = schema.List(
+ title="Official Reviewers",
+ description="Users who are official reviewers",
+ value_type=schema.TextLine(),
+ required=False,
+ )
+
+ unofficial_reviewers = schema.List(
+ title="Unofficial Reviewers",
+ description="Users who are unofficial reviewers",
+ value_type=schema.TextLine(),
+ required=False,
+ )
+```
+
+Register the behavior in ZCML with annotation storage:
+
+```xml
+
+```
+
+The `AnnotationStorage` factory automatically stores field values in annotations, using the behavior interface's identifier as the annotation key.
+
+
+### Cleaning up content annotations
+
+```{warning}
+If you store Python objects in annotations, you need to clean them up during add-on uninstallation.
+Otherwise, if the Python code is removed, you can no longer import or export the Plone site.
+Annotations are pickled objects in the database, and pickles don't work if the code is not present.
+```
+
+Here's how to clean up annotations on content objects:
+
+```python
+from io import StringIO
+from zope.annotation.interfaces import IAnnotations
+from plone.dexterity.interfaces import IDexterityContainer
+
+
+def clean_up_content_annotations(portal, names):
+ """Remove annotation entries from content objects in a Plone site.
+
+ This is useful for removing objects that might make the site
+ un-exportable when add-on code has been removed.
+
+ Args:
+ portal: Plone site object
+ names: List of annotation key names to remove
+
+ Returns:
+ StringIO with log output
+ """
+ output = StringIO()
+
+ def recurse(context):
+ """Recurse through all content on the Plone site."""
+ annotations = IAnnotations(context)
+
+ for name in names:
+ if name in annotations:
+ print(
+ f"Cleaning up annotation {name} on item {context.absolute_url()}",
+ file=output
+ )
+ del annotations[name]
+
+ # Only recurse into actual folders
+ if IDexterityContainer.providedBy(context):
+ for item_id, item in context.contentItems():
+ recurse(item)
+
+ recurse(portal)
+ return output
+```
+
+You can call this from an uninstall profile or upgrade step:
+
+```python
+def uninstall(context):
+ """Uninstall handler."""
+ portal = context.getSite()
+ clean_up_content_annotations(
+ portal,
+ ["mypackage.annotation.key1", "mypackage.annotation.key2"]
+ )
+```
+
+
+### Make your code persistence-free
+
+There's an issue with custom annotation classes: they create new persistent classes, so your data requires your source code.
+This makes your code hard to uninstall, requiring both backward compatibility code and database cleanup.
+
+An alternative pattern is to use existing persistent base classes instead of creating your own:
+
+- `BTrees` (for large data sets)
+- `persistent.list.PersistentList`
+- `persistent.mapping.PersistentMapping`
+
+Example using `PersistentMapping`:
+
+```python
+from persistent.mapping import PersistentMapping
+from zope.annotation.interfaces import IAnnotations
+
+KEY = "mypackage.likes"
+
+
+def get_likes_for(item):
+ """Get likes data using PersistentMapping.
+
+ This approach doesn't require custom persistent classes.
+ """
+ annotations = IAnnotations(item)
+
+ if KEY not in annotations:
+ annotations[KEY] = PersistentMapping({
+ "likes": set(),
+ "dislikes": set(),
+ })
+
+ return annotations[KEY]
+
+
+def like_item(item, user_id):
+ """Record a like from a user."""
+ data = get_likes_for(item)
+ data["dislikes"].discard(user_id)
+ data["likes"].add(user_id)
+
+
+def dislike_item(item, user_id):
+ """Record a dislike from a user."""
+ data = get_likes_for(item)
+ data["likes"].discard(user_id)
+ data["dislikes"].add(user_id)
+
+
+def get_status(item):
+ """Get the like/dislike counts."""
+ data = get_likes_for(item)
+ return len(data["likes"]), len(data["dislikes"])
+```
+
+This pattern is used by add-ons such as `cioppino.twothumbs` and `collective.favoriting`.
+
+
+## Related content
+
+- [`zope.annotation` on PyPI](https://pypi.org/project/zope.annotation/)
+- {doc}`behaviors`
+- {doc}`content-types/index`
diff --git a/docs/classic-ui/templates.md b/docs/classic-ui/templates.md
index b9c012ab6..166214e29 100644
--- a/docs/classic-ui/templates.md
+++ b/docs/classic-ui/templates.md
@@ -1,13 +1,974 @@
---
myst:
html_meta:
- "description": ""
- "property=og:description": ""
- "property=og:title": ""
- "keywords": ""
+ "description": "Page Templates in Plone Classic UI using TAL, TALES, and METAL"
+ "property=og:description": "Page Templates in Plone Classic UI using TAL, TALES, and METAL"
+ "property=og:title": "Templates"
+ "keywords": "Plone, Classic UI, templates, TAL, TALES, METAL, Chameleon, page templates"
---
(classic-ui-templates-label)=
# Templates
+Page Templates are the primary way to generate HTML output in Plone Classic UI.
+They are HTML files enhanced with special attributes written in TAL (Template Attribute Language), TALES (TAL Expression Syntax), and METAL (Macro Expansion for TAL).
+
+Plone uses [Chameleon](https://chameleon.readthedocs.io/) as its template engine, integrated through the Zope framework.
+Chameleon is a fast HTML/XML template engine that implements the ZPT (Zope Page Templates) specification with additional features.
+
+
+(templates-basics-label)=
+
+## Template basics
+
+A Page Template is a valid HTML or XML file with special `tal:`, `metal:`, and `i18n:` attributes that control how the template is rendered.
+
+Here is a minimal example:
+
+```html
+
+
+
+
+
Hello, World!
+
Placeholder title
+
+
+
+
+```
+
+The three parts serve different purposes:
+
+TAL (Template Attribute Language)
+: Controls the structure and content of the output.
+ TAL attributes like `tal:content`, `tal:repeat`, and `tal:condition` modify how elements are rendered.
+
+TALES (TAL Expression Syntax)
+: Defines the syntax for expressions used in TAL attributes.
+ TALES supports path expressions, Python expressions, string expressions, and more.
+
+METAL (Macro Expansion for TAL)
+: Enables template reuse through macros and slots.
+ Use METAL to inherit from base templates and define reusable template fragments.
+
+
+(templates-filesystem-vs-ttw-label)=
+
+## Filesystem vs. TTW templates
+
+Templates in Plone can be stored in two locations, with important security implications:
+
+**Filesystem templates** (recommended)
+: Templates stored in your add-on package's directory structure, typically in `browser/templates/` or `views`.
+ These templates run as **trusted code** with full Python capabilities, just like any other code in your package.
+ They have no security restrictions on Python expressions.
+
+**TTW (Through-The-Web) templates**
+: Templates created and stored in the ZODB through the Zope Management Interface (ZMI).
+ These templates run with **RestrictedPython** sandboxing for security.
+ They have limited Python functionality: no `import` statements, restricted builtins, and security-checked attribute access.
+
+```{note}
+Always develop templates on the filesystem in your add-on package.
+TTW templates are discouraged for production code because they are harder to maintain, version control, and test.
+```
+
+
+(templates-tal-label)=
+
+## TAL statements
+
+TAL uses special attributes to control template rendering.
+When an element has multiple TAL attributes, they execute in this order:
+
+1. `tal:define`
+2. `tal:condition`
+3. `tal:repeat`
+4. `tal:content` or `tal:replace`
+5. `tal:attributes`
+6. `tal:omit-tag`
+
+
+(templates-tal-define-label)=
+
+### `tal:define`
+
+Defines one or more variables for use in the template.
+
+```html
+
+
Portal URL: ${portal_url}
+
User: ${user/getId}
+
+```
+
+Use semicolons to define multiple variables.
+Variables are available within the element and its children.
+
+For global variables accessible throughout the template, use `tal:define="global varname expression"`.
+
+
+(templates-tal-condition-label)=
+
+### `tal:condition`
+
+Conditionally includes or excludes an element and its children.
+
+```html
+
+ ${context/description}
+
+
+
+ Found ${python:len(items)} items.
+
+
+
+
+ No description available.
+
+```
+
+If the condition evaluates to a false value, the entire element and all its children are removed from the output.
+
+
+(templates-tal-repeat-label)=
+
+### `tal:repeat`
+
+Repeats an element for each item in a sequence.
+
+```html
+
+
+ Item ${item}
+
+
+
+
+
+
${brain/Title}
+
${brain/Description}
+
+
+```
+
+Inside a repeat loop, you have access to the `repeat` variable which provides information about the current iteration:
+
+| Variable | Description |
+|----------|-------------|
+| `repeat/item/index` | Zero-based index (0, 1, 2, ...) |
+| `repeat/item/number` | One-based index (1, 2, 3, ...) |
+| `repeat/item/even` | True for even indices |
+| `repeat/item/odd` | True for odd indices |
+| `repeat/item/start` | True for the first item |
+| `repeat/item/end` | True for the last item |
+| `repeat/item/length` | Total number of items |
+| `repeat/item/letter` | Lowercase letter (a, b, c, ...) |
+| `repeat/item/Letter` | Uppercase letter (A, B, C, ...) |
+
+Example using repeat variables:
+
+```html
+
+
+
${repeat/item/number}
+
${item/title}
+
+
+```
+
+
+(templates-tal-content-label)=
+
+### `tal:content`
+
+Replaces the content of an element with the expression result.
+
+```html
+
Placeholder Title
+
+
+ This placeholder text will be replaced.
+
+```
+
+By default, content is HTML-escaped.
+To insert raw HTML, use the `structure` keyword:
+
+```html
+
+ Raw HTML will be inserted here.
+
+```
+
+
+(templates-tal-replace-label)=
+
+### `tal:replace`
+
+Replaces the entire element (not just its content) with the expression result.
+
+```html
+Placeholder
+
+
+
+
+```
+
+
+(templates-tal-attributes-label)=
+
+### `tal:attributes`
+
+Sets or modifies HTML attributes dynamically.
+
+```html
+
+ ${context/title}
+
+
+
+
+
+ ...
+
+```
+
+Setting an attribute to `None` removes it from the output.
+
+
+(templates-tal-omit-tag-label)=
+
+### `tal:omit-tag`
+
+Removes the element tag but keeps its content.
+
+```html
+
+ This text appears without any wrapper.
+
+
+
+
+```
+
+
+(templates-tal-block-label)=
+
+### Pure TAL blocks
+
+When you need TAL logic without generating HTML elements, use the `tal:block` style elements:
+
+```html
+
+
+
${item/Title}
+
+
+```
+
+You can use any tag name with the `tal:` prefix:
+
+```html
+
+
${item/Title}
+
+```
+
+
+(templates-tal-switch-label)=
+
+### `tal:switch` and `tal:case` (Chameleon extension)
+
+Chameleon provides switch/case statements as an extension to standard TAL:
+
+```html
+
+
This is a document.
+
This is a news item.
+
This is an event.
+
This is something else.
+
+```
+
+
+(templates-tales-label)=
+
+## TALES expressions
+
+TALES (TAL Expression Syntax) defines how expressions are evaluated in templates.
+Plone uses **path expressions** as the default expression type.
+
+
+(templates-tales-path-label)=
+
+### Path expressions
+
+Path expressions traverse object attributes and items using `/` as a separator.
+
+```html
+
+
+```
+
+```{warning}
+The `|` operator catches missing attributes and `None` values, but it also catches other errors.
+Use it sparingly to avoid hiding bugs.
+A typo like `context/ttle` (missing 'i') will silently use the fallback.
+```
+
+
+(templates-tales-python-label)=
+
+### Python expressions
+
+Python expressions allow full Python syntax.
+They must be prefixed with `python:`.
+
+```html
+
TITLE
+
+
+ Showing first 10 of ${python:len(items)} items.
+
+
+
+
+ ${item/title}
+
+
+
+
+
+ Title (Type)
+
+```
+
+In filesystem templates, you have full Python access.
+In TTW templates, RestrictedPython limits what you can do.
+
+```{important}
+In Plone templates, the default expression type is `path:`, not `python:`.
+You must explicitly use the `python:` prefix for Python expressions.
+This differs from standalone Chameleon where Python is the default.
+```
+
+
+(templates-tales-string-label)=
+
+### String expressions
+
+String expressions create formatted strings with variable interpolation.
+
+```html
+
+ Edit
+
+
+
+ Greeting
+
+
+
+```
+
+Inside `${...}` you can use any TALES expression, but the default is path.
+
+
+(templates-tales-not-label)=
+
+### The `not:` expression
+
+Negates a boolean expression.
+
+```html
+
+ No description available.
+
+
+
+ No items found.
+
+```
+
+
+(templates-tales-exists-label)=
+
+### The `exists:` expression
+
+Tests whether a path exists without evaluating it.
+
+```html
+
+ Custom field exists: ${context/custom_field}
+
+```
+
+
+(templates-tales-nocall-label)=
+
+### The `nocall:` expression
+
+Returns an object without calling it.
+
+```html
+
+
+ Method: ${python:method.__name__}
+
+```
+
+
+(templates-built-in-variables-label)=
+
+## Built-in variables
+
+Plone templates have access to several built-in variables:
+
+| Variable | Description |
+|----------|-------------|
+| `context` | The content object the view is called on |
+| `view` | The browser view instance |
+| `request` | The current HTTP request object |
+| `template` | The template object itself |
+| `options` | Additional options passed to the template |
+| `nothing` | Equivalent to Python's `None` |
+| `default` | Special value to keep the original content |
+| `repeat` | Dictionary of repeat variables in loops |
+| `attrs` | Original attributes of the current element (in METAL) |
+
+Additional variables available through helper views:
+
+```html
+
+
Portal URL: ${portal_url}
+
User: ${user/getId}
+
Please log in.
+
+```
+
+
+(templates-metal-label)=
+
+## METAL macros
+
+METAL enables template reuse through macros and slots.
+
+
+(templates-metal-define-macro-label)=
+
+### Defining macros
+
+Create a reusable template fragment:
+
+```html
+
+
+
+ ${user/fullname}
+ ${user/email}
+
+
+```
+
+
+(templates-metal-use-macro-label)=
+
+### Using macros
+
+Include a macro in another template:
+
+```html
+
+
+
+```
+
+
+(templates-best-practices-escape-label)=
+
+### Always escape user content
+
+Content is escaped by default.
+Only use `structure` when you trust the content:
+
+```html
+
+
User input
+
+
+
Rich text
+```
+
+
+(templates-best-practices-fallbacks-label)=
+
+### Provide fallbacks
+
+Handle missing or empty values gracefully:
+
+```html
+
+ Description
+
+
+
+```
+
+
+(templates-debugging-label)=
+
+## Debugging templates
+
+
+(templates-debugging-variables-label)=
+
+### Inspecting available variables
+
+Use `context` to see all available variables:
+
+```html
+
+
${name}
+
${python:repr(context[name])[:100]}
+
+```
+
+
+(templates-debugging-pdb-label)=
+
+### Using the debugger
+
+In filesystem templates, you can use pdb:
+
+```html
+
+```
+
+
+(templates-debugging-errors-label)=
+
+### Common errors
+
+**`KeyError` or `AttributeError`**
+: Check for typos in path expressions.
+ Use `exists:` to test if a path exists.
+
+**Unexpected output**
+: Remember that `tal:content` replaces content, `tal:replace` replaces the entire element.
+
+**Expression not evaluated**
+: Ensure you're using the correct expression type prefix (`python:`, `string:`).
+
+**Encoding errors**
+: Ensure your template files are saved as UTF-8.
+
+
+(templates-reference-label)=
+
+## Quick reference
+
+
+(templates-reference-tal-label)=
+
+### TAL statements
+
+| Statement | Purpose | Example |
+|-----------|---------|---------|
+| `tal:define` | Define variables | `tal:define="title context/title"` |
+| `tal:condition` | Conditional rendering | `tal:condition="context/description"` |
+| `tal:repeat` | Loop over items | `tal:repeat="item items"` |
+| `tal:content` | Replace element content | `tal:content="context/title"` |
+| `tal:replace` | Replace entire element | `tal:replace="context/title"` |
+| `tal:attributes` | Set HTML attributes | `tal:attributes="href context/absolute_url"` |
+| `tal:omit-tag` | Remove element, keep content | `tal:omit-tag=""` |
+| `tal:on-error` | Error handling | `tal:on-error="string:Error"` |
+
+
+(templates-reference-tales-label)=
+
+### TALES expression types
+
+| Type | Prefix | Default in Plone | Example |
+|------|--------|------------------|---------|
+| Path | `path:` | Yes | `context/title` |
+| Python | `python:` | No | `python:len(items)` |
+| String | `string:` | No | `string:Hello ${name}` |
+| Not | `not:` | No | `not:context/description` |
+| Exists | `exists:` | No | `exists:context/image` |
+| Nocall | `nocall:` | No | `nocall:context/method` |
+
+
+(templates-reference-metal-label)=
+
+### METAL statements
+
+| Statement | Purpose | Example |
+|-----------|---------|---------|
+| `metal:define-macro` | Define reusable macro | `metal:define-macro="card"` |
+| `metal:use-macro` | Use a macro | `metal:use-macro="context/@@macros/card"` |
+| `metal:define-slot` | Define customizable slot | `metal:define-slot="content"` |
+| `metal:fill-slot` | Fill a slot | `metal:fill-slot="content"` |
+
+
+## See also
+
+- {doc}`views`
+- {doc}`viewlets`
+- {doc}`template-global-variables`
+- [Chameleon documentation](https://chameleon.readthedocs.io/)
+- [Plone Training: Page Templates](https://training.plone.org/mastering-plone-5/zpt.html)
diff --git a/docs/classic-ui/views.md b/docs/classic-ui/views.md
index e2a7097be..273b9a5a0 100644
--- a/docs/classic-ui/views.md
+++ b/docs/classic-ui/views.md
@@ -715,17 +715,17 @@ However, in most cases:
Replace links to Plone 5.2 docs with links to Plone 6 docs.
Specifically:
-- [TAL page template](https://5.docs.plone.org/adapt-and-extend/theming/templates_css/template_basics)
-- [interface](https://5.docs.plone.org/develop/addons/components/interfaces)
+- {doc}`TAL page template `
+- {doc}`interface `
```
- Full Plone page views are a subclass of [`Products.Five.browser.BrowserView`](https://github.com/zopefoundation/Zope/blob/d1814d0a6bddb615629b552de10e9aa5ad30a6da/src/Products/Five/browser/__init__.py#L20) which is a wrapper class.
It wraps [`zope.publisher.browser.BrowserView`](https://github.com/zopefoundation/zope.publisher/blob/dea3d4757390d04f6a5b53e696f08d0cab5f6023/src/zope/publisher/browser.py#L958), and adds an acquisition (parent traversal) support for it.
-- Views have an attribute `index`, which points to a [TAL page template](https://5.docs.plone.org/adapt-and-extend/theming/templates_css/template_basics) that is responsible for rendering the HTML code.
+- Views have an attribute `index`, which points to a {doc}`TAL page template ` that is responsible for rendering the HTML code.
You get the HTML output with `self.index()`.
The page template gets a context argument `view`, pointing to the view class instance.
The `index` value is usually an instance of [`Products.Five.browser.pagetemplate.ViewPageTemplateFile`](https://github.com/zopefoundation/Zope/blob/d1814d0a6bddb615629b552de10e9aa5ad30a6da/src/Products/Five/browser/pagetemplatefile.py#L35) for full Plone pages or [`zope.pagetemplate.pagetemplatefile.PageTemplateFile`](https://github.com/zopefoundation/zope.pagetemplate/blob/14ba59c98e12517b9f8abcdb24bc882bb435ed7c/src/zope/pagetemplate/pagetemplatefile.py#L43) for HTML snippets without using acquisition.
-- View classes should implement the [interface](https://5.docs.plone.org/develop/addons/components/interfaces)
+- View classes should implement the {doc}`interface `
[`zope.browser.interfaces.IBrowserView`](https://github.com/zopefoundation/zope.browser/blob/1239c75e4e190df992bf34a88b4ead2c952afe86/src/zope/browser/interfaces.py#L27).
Views that render page snippets and parts can be direct subclasses of `zope.publisher.browser.BrowserView`, as snippets might not need acquisition support which adds some overhead to the rendering process.
@@ -1126,4 +1126,4 @@ One workaround to avoid this mess is to use `aq_inner` when accessing `self.obj`
- {doc}`/classic-ui/layers`
- {doc}`/classic-ui/templates`
-- {doc}`/classic-ui/viewlets`
\ No newline at end of file
+- {doc}`/classic-ui/viewlets`