Composing pages

← All sections · part of the machine-readable /all/ index.

Composing pages

Composing pages

Reference for the HTML patterns that assemble pages from parts — bindings that compute values, templates that render lists, includes that pull content from other documents, filters that transform values, and elements that exist only during composition.

Most of these are schema-free: they work on plain HTML and the site graph with no modeled type at all. One is schema-poweredMethod Elements invoke a Method declared on a schema, so they require modeling a type first. See What modeling unlocks in pages for the progression.

These same composition patterns also work for XML documents — RSS/Atom feeds, sitemaps, SVG, and XHTML (any +xml content type, plus application/xml and text/xml). An XML document is parsed and composed with the same directives — including microdata @read resolution — and served as well-formed XML (empty elements self-close, tag/attribute case is preserved, and the <?xml … ?> declaration is kept). The HTML-only conveniences that do not apply are JSON-LD content negotiation and xmlns: declaration stripping. See XML documents for the full reference.

Pages in this group

See also

Resource Binding

Resource Binding

A resource binding declares a site-wide CSS selector query and exposes the matching elements as a named variable for use by templates, constraints, triggers, and other server-side processors.

When to reach for it

Use resource bindings to pull data from across the site graph into a single document. Templates render the bound elements, constraints validate invariants against them, and triggers access context through them.

Namespace declaration

Declare the Pagelove Resource namespace on any element:

<html xmlns:r="https://pagelove.org/Binding/CSS">

The prefix can be any valid XML prefix (r, resource, etc.). Server-side processing must also be enabled by declaring the https://pagelove.org/1.0 namespace:

<html xmlns:p="https://pagelove.org/1.0"
      xmlns:r="https://pagelove.org/Binding/CSS">

Attribute form

Add a namespaced attribute to any element. The attribute name is the variable name. The attribute value is a CSS selector evaluated across the entire site graph.

r:<name>="<css-selector>"

The result is a collection of matching elements.

Semantics

Property Behaviour
Scope Site-wide — selectors operate across all documents
Result type HTML elements, not deserialized records
Evaluation At processing time, not cached
Mutability Read-only — mutation is performed via HTTP methods

Binding placement

Bindings may appear on any element. The declaring element determines scope:

Multiple bindings may appear on the same element.

Examples

Template rendering

The following document binds all schema.org/Person items to a variable named contacts and renders them with a Liquid template:

<!doctype html>
<html lang="en"
      xmlns:r="https://pagelove.org/Binding/CSS"
      xmlns:p="https://pagelove.org/1.0">

<head>
    <title>Contacts List</title>
</head>

<body r:contacts="[itemtype='http://schema.org/Person']"
      p:template="text/liquid">
    <ul>
        {% for contact in contacts %}
        <li>{{ contact.name }}</li>
        {% endfor %}
    </ul>
</body>
</html>

The selector [itemtype='http://schema.org/Person'] evaluates across all documents. Matching elements bind to contacts. The template iterates over contacts to render the list.

Graph constraint

Resource bindings supply the variables that constraint expressions operate on:

<body xmlns:r="https://pagelove.org/Binding/CSS"
      r:users="[itemtype*=User]"
      r:admins="[itemtype*=User]:has([itemprop=role]:value-equals('admin'))">

  <div itemscope itemtype="https://pagelove.org/Constraint">
    <code itemprop="constraint">size(admins) >= 1</code>
    <span itemprop="message">At least one admin user must exist</span>
  </div>

</body>

Trigger context

Triggers use resource bindings to access data for transformations:

<div xmlns:r="https://pagelove.org/Binding/CSS"
     r:auth="[itemtype*=Request] [itemprop=auth]"
     r:apiKey="[itemprop=webhook-api-key]"
     itemscope
     itemtype="https://pagelove.org/Trigger">

    <meta itemprop="selector" content="[itemtype*=Order]">
    <meta itemprop="method" content="PUT">

    <code itemprop="transformation">
      {
        headers: {
          'Authorization': 'Bearer ' + apiKey
        },
        body: {
          purchaser: auth.claims.email
        }
      }
    </code>

    <div itemprop="action" itemscope itemtype="https://pagelove.org/HTTPRequest">
      <meta itemprop="url" content="https://api.example.com/orders">
    </div>
</div>

The transformation expression accesses auth and apiKey bindings directly by name.

Note — binding the Request Document. r:auth="[itemtype*=Request] …" sources data from the Request Document, a transient, selector-addressable document representing the current request. The binding resolves during page composition, and any page that reads a request-document fragment is served Cache-Control: private. If you only need a field (not a selector-addressable fragment), the request object is also available directly in expression bindings and Liquid templates — request.auth.claims.* / request.auth.username (see Sessel syntax → Authenticated identity in composition).

Security

Resource bindings are evaluated server-side during composition and query the entire site graph without per-request authorization. A binding reads matching elements from every document on the host, regardless of whether the current request would be authorized to fetch those documents directly. Authorization rules govern which requests may reach which paths — they do not restrict what a resource binding may read.

The trust boundary is therefore who may author a composed page: because a page's bindings can surface any content stored on the host, treat authorship of a composed page as equivalent to read access over the whole host. Do not use a resource binding to pull data onto a page whose authors should not be able to see that data.

Error cases

Condition Result
Invalid CSS selector Request fails during document processing
Selector matches no elements Empty collection (not an error)
Selector matches elements in other documents All matching elements are returned — bindings are not filtered by per-request authorization (see Security)

See also

Sessel expression binding

Sessel expression binding

A Sessel expression binding evaluates a Sessel expression during server-side composition and exposes the result as a named variable to templates. For the JavaScript peer, see JavaScript expression binding.

When to reach for it

Use expression bindings to compute values — averages, filtered lists, counts — directly in HTML without application code. They complement resource bindings, which select elements but cannot compute over them.

Namespace declaration

Declare the Sessel binding namespace on an ancestor element:

<html xmlns:e="https://pagelove.org/Binding/Sessel">

The namespace URI must be exactly https://pagelove.org/Binding/Sessel. The prefix (here e) can be any valid XML prefix.

Attribute form

Part Role
Prefix (e:) The declared namespace prefix
Attribute name The variable name exposed to templates
Attribute value A Sessel expression evaluated at serve time
<div e:total="${[itemprop="price"]}.sum()"
     e:count="${[itemprop="price"]}.count()"
     pagelove:template="text/liquid">
  <p>{{ count }} products totalling ${{ total }}</p>
</div>

Declaration-order evaluation

Attributes evaluate left to right in source order. A bound name is available to subsequent expressions on the same element:

<div e:total="${[itemprop="price"]}.sum()"
     e:count="${[itemprop="price"]}.count()"
     e:average="total / count"
     pagelove:template="text/liquid">
  <p>Average price: ${{ average }}</p>
</div>

Here total and count resolve first. When average evaluates, both names are already in scope.

Scope

Bindings follow element ancestry:

<section xmlns:e="https://pagelove.org/Binding/Sessel"
         e:sitecount="${div.item}.count()">

  <!-- sitecount is available here and in all descendants -->
  <div e:localcount="${div.item} from self).count()"
       pagelove:template="text/liquid">
    <!-- Both sitecount and localcount are available -->
    <p>{{ localcount }} of {{ sitecount }} items</p>
  </div>

  <div pagelove:template="text/liquid">
    <!-- sitecount is available, localcount is NOT (it is on a sibling) -->
    <p>{{ sitecount }} total items</p>
  </div>

</section>

The three bindings compared

Sessel is one of three binding namespaces that attach a named value to an element during composition:

Aspect Resource binding Sessel expression binding JavaScript expression binding
Namespace https://pagelove.org/Binding/CSS https://pagelove.org/Binding/Sessel https://pagelove.org/Binding/JavaScript
Value CSS selector (returns matching elements) Sessel expression (returns any value) JavaScript expression (returns any value)
Can compute No — selects elements only Yes — arithmetic, aggregation, filtering Yes — full JavaScript expression
Can reference other bindings No Yes — later bindings can reference earlier ones Yes — earlier bindings read as bare identifiers

All three may appear on the same element. Resource bindings resolve first, then expression bindings evaluate in declaration order.

Examples

Count and sum from site data

Expression bindings can aggregate values across multiple documents. Given two product pages, a summary page uses count() and sum() to compute totals:

{% example "setup-summary", "body" %}

Fetching the summary page renders the computed values:

GET /sessel-expr-summary.html
HTTP/1.1 200

<!DOCTYPE html>
<html>
<body>
  <div>
    <p>Count: 2</p>
    <p>Sum: 100.0</p>
  </div>
</body>
</html>

Error cases

Condition Result
Sessel expression fails to compile Request fails with an error during composition
Expression references an undefined variable name Request fails with an error during composition
Namespace URI is not exactly https://pagelove.org/Binding/Sessel Attributes are ignored — no bindings are created

See also

JavaScript expression binding

JavaScript expression binding

A JavaScript expression binding evaluates a single JavaScript expression during server-side composition and exposes its value as a named variable — the JavaScript peer of the Sessel expression binding (e:). The expression runs server-side in dombase-js; for the broader server-side JavaScript surface see JavaScript in schemas and the JavaScript DOM API.

Namespace declaration

Declare the JavaScript binding namespace on an ancestor element:

<html xmlns:j="https://pagelove.org/Binding/JavaScript">

The namespace URI must be exactly https://pagelove.org/Binding/JavaScript. The prefix (here j) can be any valid XML prefix.

Attribute form

Each j:name="<expr>" attribute evaluates <expr> as one JavaScript expression and exposes its value under name. Attributes evaluate in declaration order, and existing bindings are visible to later expressions as bare identifiers:

<ul j:total="[10, 20, 30].reduce((a, b) => a + b, 0)"
    j:doubled="total * 2"
    pagelove:template="text/liquid">
  <li>Total: {{ total }}</li>
  <li>Doubled: {{ doubled }}</li>
</ul>

Here total is computed first with a JavaScript reduce, then doubled reads it as a plain identifier. The expression may also use await, so it can resolve a Promise before its value is bound.

In action

Given the page above stored on the host:

{% example "setup-js-summary", "body" %}

Fetching it renders the computed values:

GET /js-expr-summary.html
HTTP/1.1 200

<!DOCTYPE html>
<html>
<body>
  <ul>
    <li>Total: 60</li>
    <li>Doubled: 120</li>
  </ul>
</body>
</html>

What the expression can see

A j: expression runs as the body of one JavaScript function. In scope are:

A name is available as a bare identifier only when it is a plain JavaScript identifier and not a reserved word. A binding named after a keyword or with a non-identifier name (j:class, j:new, j:"first name") is still computed, but a later expression must read it as Context["class"] rather than a bare class.

Caching. Reading a per-user field of requestrequest.auth, request.headers, and other identity members — marks the response as user-varying, so it is served Cache-Control: private and excluded from the shared cache. Reading only shared fields (request.path, request.query, request.method) keeps the page publicly cacheable. Naming request without reading an identity member does not taint it.

Only the attribute form

Like e: Sessel bindings, JavaScript bindings exist only in the attribute form j:name="…". There is no <j:…> element form, and no standalone JavaScript expression type to declare on a schema — the j: attribute is the whole surface.

Sessel or JavaScript?

e: and j: are peers: each computes a value during composition and exposes it to later bindings and templates. Reach for:

They interleave freely on one element; use whichever reads best for each value. See the three bindings compared.

Error cases

Condition Result
JavaScript expression fails to parse, throws, or rejects Request fails with an HTML-Microdata error during composition
Namespace URI is not exactly https://pagelove.org/Binding/JavaScript j: attributes are ignored — no bindings are created

See also

Pagination

Pagination

The p:paginate attribute splits an element's direct children across numbered pages during server-side composition. Children outside the current page are removed from the response, and navigation links are generated automatically.

When to reach for it

Use pagination when a composed element produces more children than should appear in a single response. Pagination runs after includes, expression bindings, and templates have evaluated — it operates on the fully-resolved DOM.

Attribute form

<ul id="items" xmlns:p="https://pagelove.org/1.0" p:paginate="10">
  <li>Item 1</li>
  <li>Item 2</li>
  <!-- ... -->
</ul>

The attribute value is a positive integer specifying the default page length. When multiple paginators exist in the same document, each element must have an id attribute.

Query parameters

Clients navigate pages using query parameters. Pages are 1-indexed.

Parameter Purpose Default
paginate:page Page number 1
paginate:length Items per page Attribute value

For a single paginator:

/contacts.html?paginate:page=2
/contacts.html?paginate:page=2&paginate:length=20

When multiple paginators exist, prefix with the element's id:

/dashboard.html?paginate:users:page=2&paginate:posts:page=3

Invalid values (non-numeric, zero, negative) fall back to defaults silently. A requested page beyond the total is clamped to the last page.

Pagination generates <link> elements in <head> and HTTP Link headers (RFC 8288) for navigation:

GET /paginate-docs/contacts.html?paginate:page=2&paginate:length=3
HTTP/1.1 200

<!DOCTYPE html>
<html>
<head><title>Contacts</title><link rel="first" href="?paginate:page=1&amp;paginate:length=3" title="contacts"><link rel="prev" href="?paginate:page=1&amp;paginate:length=3" title="contacts"><link rel="next" href="?paginate:page=3&amp;paginate:length=3" title="contacts"><link rel="last" href="?paginate:page=3&amp;paginate:length=3" title="contacts"></head>
<body>
<ul id="contacts">



<li>Dave</li>
<li>Eve</li>
<li>Frank</li>

</ul>
</body>
</html>

Generated links include rel="first", rel="prev", rel="next", and rel="last" as appropriate. All non-pagination query parameters are preserved in the generated URLs.

Examples

Single page

When all children fit on one page, no navigation links are generated:

GET /paginate-docs/short.html
HTTP/1.1 200

<!DOCTYPE html>
<html>
<head><title>Short List</title></head>
<body>
<ul id="items">
<li>Alpha</li>
<li>Beta</li>
<li>Gamma</li>
</ul>
</body>
</html>

With Range selectors

Pagination works with the Range header. When a request includes Range: selector=ul#contacts, the full document is composed first (pagination, templates, bindings), then the selector extracts the paginated fragment:

GET /paginate-docs/contacts.html?paginate:page=2&paginate:length=3
Range: selector=ul#contacts
HTTP/1.1 206
Content-Range: selector=ul#contacts

<ul id="contacts">



<li>Dave</li>
<li>Eve</li>
<li>Frank</li>

</ul>

This supports AJAX-style page navigation — fetch the paginated list fragment without the surrounding document.

Multiple paginators

A document can contain multiple independently paginated elements. Each must have a unique id:

<section>
  <ul id="users" p:paginate="5"><!-- user items --></ul>
  <ul id="posts" p:paginate="10"><!-- post items --></ul>
</section>

Navigate them independently:

?paginate:users:page=2&paginate:posts:page=3

Each paginator's generated links preserve the other paginators' current state.

Error cases

Condition Result
p:paginate value is not a positive integer 422 error response
Multiple paginators without id attributes 422 error response
Page number exceeds total pages Clamped to last page
No children in paginated element Element returned as-is, no links generated

See also

Resource Creation

Resource Creation

Pagelove provides two modes for creating new resources using standard HTTP semantics: direct creation via PUT to a known name, and templated creation via POST to a template resource.

When to reach for it

Use PUT when the client knows the final resource name and can supply the full representation. Use POST to a template when the final name or content must be determined by server-side logic.

Mode comparison

Aspect PUT POST + Template
Final resource name Known by client Determined by template (<base>)
Server-side logic None Full templating system
Authorization checks 1 (PUT) 2 (POST to template, PUT to final location)
Client receives Standard PUT response 301 redirect to new resource
Typical use Uploading files, programmatic creation Blog posts, CMS entries, user-generated content

Direct creation (PUT)

The client sends an HTTP PUT request to the desired resource path:

PUT /new-blog-post.html

The request body contains the full representation (HTML, image, etc.). The server verifies that the actor is authorized to PUT at the provided path using AuthorizationRule declarations.

Standard HTTP semantics apply (ETag, conditional requests, content negotiation).

Templated creation (POST)

Templated creation follows a multi-step pipeline:

1. POST to template

The client sends a POST to a template resource:

POST /templates/new-post.html

The request body, query parameters, and other request metadata are available to the template execution context.

2. Template authorization

The server verifies POST authorization on the template resource using AuthorizationRule. If this check fails, the request is rejected before any templating occurs.

3. Template processing

The template is processed using the Pagelove templating system. It can incorporate request body parameters, query parameters, headers, and actor identity. The output is a complete HTML document.

4. Final location via <base>

The generated document must contain an HTML <base> element with an href attribute:

<base href="/posts/hello-world.html">

Pagelove interprets this value as the canonical storage location of the new resource. This aligns with the standard semantics of <base>, which defines the base URL for resolving relative URLs.

5. Final location authorization

The server verifies that the actor is authorized to PUT at the <base> href. This is a separate check from the initial POST authorization. If it fails, no resource is written.

6. Persistence and redirect

The generated document is written to the content store at the path specified by base.href. The server responds with:

301 Moved Permanently
Location: 

Error cases

Condition Result
POST to the template is not authorized Rejected before templating runs, per AuthorizationRule.
Templated output has no <base> element, an empty href, or no href attribute at all 422 Unprocessable Entity — "Template must include a <base href> element specifying the target resource path".
The actor is not authorized to PUT at the resolved <base href> Rejected; no resource is written.
The request body cannot be fully read (the connection is interrupted mid-upload) 400 Bad Request; no resource is written. A partially-received body is never composed and stored, so a dropped connection returns an error you can retry rather than silently publishing a truncated document.

Examples

Templated creation via POST

Store a template resource that uses the request body to build the new document and determines its location via <base>:

{% example "setup-template", "body" %}

POST form data to the template. The server creates the resource and responds with a redirect:

POST /sspi-rc-templates/new-entry.html
Content-Type: application/x-www-form-urlencoded

slug=hello-world&title=Hello+World&content=First+post
HTTP/1.1 301
Location: /sspi-rc-entries/hello-world.html

See also

Selector Extensions

Selector Extensions

Pagelove extends CSS selectors with pseudo-classes and functions for querying HTML as data. These extensions enable text matching, microdata comparison, numeric filtering, structure validation, and cross-document queries.

In addition to the extensions below, Pagelove supports CSS Selectors Level 4 features including :has(), :is(), :where(), :not(), and all standard structural pseudo-classes. Namespace-aware selectors ([prefix|attr], prefix|tagname) are supported via the Namespace header.

Quick reference

Pseudo-class Operates on Match type Case flag
:contains(text) text content substring , i
:equals(text) text content exact , i
:value-contains(text) microdata value substring , i
:value-equals(text) microdata value exact , i
:less-than(n) text content numeric < --
:greater-than(n) text content numeric > --
:value-less-than(n) microdata value numeric < --
:value-greater-than(n) microdata value numeric > --
:only(selectors) descendants structure --
:isa(url) itemtype type inheritance --
Function Arguments Returns
count(selector) CSS selector number
text-of(selector) CSS selector quoted text content
value-of(selector) CSS selector quoted microdata value
attr-of(attr, selector) attribute name, CSS selector quoted attribute value

Text content vs microdata values

Text content is the concatenated, normalized text inside an element and its descendants.

Microdata value follows the WHATWG HTML Microdata specification. The value source depends on the element type:

Element Value source
meta content attribute
audio, embed, iframe, img, source, track, video src attribute
a, area, link href attribute
object data attribute
data, meter value attribute
time datetime attribute, falls back to text content
Everything else Descendant text content

When a spec-listed element is missing its designated attribute, the microdata value is an empty string — except <time>, which falls back to text content.

<!-- Text content: empty. Microdata value: "John Doe" (content attr) -->
<meta itemprop="name" content="John Doe">

<!-- Text content: "Click here". Microdata value: "/page" (href attr) -->
<a itemprop="url" href="/page">Click here</a>

<!-- Text content: "Forty-two". Microdata value: "42" (value attr) -->
<data itemprop="score" value="42">Forty-two</data>

The :contains() and :equals() families operate on text content. The :value-contains() and :value-equals() families operate on microdata values.

Text matching

:contains()

Matches elements whose text content includes a substring.

:contains('text')
:contains('text', i)

The i flag enables case-insensitive matching.

Selector Matches (given <h1>Hello World</h1> and <h2>hello world</h2>)
h1:contains('Hello') <h1>
h1:contains('hello') nothing (case-sensitive)
:contains('hello', i) <h1> and <h2>

:equals()

Matches elements whose text content exactly equals the given text. No substring matching.

:equals('text')
:equals('text', i)
Selector Matches (given <p>Title</p> and <p>Title Bar</p>)
p:equals('Title') First <p> only
p:contains('Title') Both

Value matching

:value-contains()

Matches elements whose microdata value includes a substring.

:value-contains('text')
:value-contains('text', i)

:value-equals()

Matches elements whose microdata value exactly equals the given text.

:value-equals('text')
:value-equals('text', i)

Useful when microdata values differ from visible text:

Selector Matches Reason
[itemprop=hostname]:value-equals('localhost') <meta itemprop="hostname" content="localhost"> content attr
[itemprop=url]:value-equals('/about') <a itemprop="url" href="/about">About Us</a> href attr
[itemprop=score]:value-equals('42') <data itemprop="score" value="42">Forty-two</data> value attr

:value-equals() combines naturally with :has() for parent-level queries:

[itemtype*=HostConfig]:has(
    [itemprop=hostname]:value-equals('localhost'),
    [itemprop=alias]:value-equals('127.0.0.1')
)

Comma-separated arguments inside :has() use OR semantics, per the CSS specification.

Numeric comparison

:less-than() and :greater-than()

Match elements whose text content, parsed as a number, is less than or greater than the threshold.

:less-than('number')
:greater-than('number')

:value-less-than() and :value-greater-than()

Match elements whose microdata value, parsed as a number, is less than or greater than the threshold.

:value-less-than('number')
:value-greater-than('number')

Numeric parsing rules

When text cannot be parsed as a number, the comparison returns false. The element is silently skipped.

Structure validation

:only()

Matches elements where every descendant matches at least one of the given selectors.

:only(selector1, selector2, ...)

Use the > prefix to require direct children rather than any descendant:

Selector <ul><li>A</li><li>B</li></ul> <ul><li>A</li><div>X</div></ul>
ul:only(> li) matches does not match

Multiple selectors combine with OR — each descendant must match at least one:

ul:only(> li, > li span)       /* direct children are li; span inside li is allowed */
div:only(> ul, > ul *)         /* direct child is ul; anything inside ul is allowed */

Elements with no descendants match vacuously.

Selector functions

Selector functions evaluate cross-document queries and replace themselves with the result. They operate across the entire site graph.

count()

Returns the number of elements matching a selector across all documents.

div:nth-child(count(h1))

If the site contains three h1 elements, this evaluates to div:nth-child(3).

text-of()

Returns the text content of a single matching element, wrapped in quotes.

[data-name=text-of(#title)]

Zero matches produce an empty string. More than one match produces an error.

value-of()

Returns the microdata value of a single matching element, wrapped in quotes. Value extraction follows the WHATWG element-specific rules above.

[data-name=value-of([itemprop='name'])]

Zero matches produce an empty string. More than one match produces an error.

attr-of()

Returns the value of a specific attribute from a single matching element, wrapped in quotes. The first argument is the attribute name (in single quotes). The second is a CSS selector.

[data-link=attr-of('href', #home)]

Zero matches produce an empty string. More than one match produces an error.

Text normalization

All text-based operations normalize text content before matching:

  1. Unicode NFC — canonical composition is applied.
  2. Trim — leading and trailing whitespace is removed.
  3. Collapse — consecutive whitespace (spaces, tabs, newlines) becomes a single space.

This means <p> Hello World\n</p> has normalized text content Hello World.

Case sensitivity

All text and value matching pseudo-classes default to case-sensitive comparison. Append , i as the last argument for case-insensitive matching. The i flag means ASCII case-insensitivity (the same semantics as the CSS attribute-selector i flag), uniformly across all four case-insensitive pseudo-classes (:contains, :equals, :value-contains, :value-equals): A-Z fold to a-z; non-ASCII letters compare exactly (é does not match É).

Numeric comparison pseudo-classes do not support the i flag — numeric comparison is inherently case-insensitive.

Quoting

Arguments to pseudo-classes accept single or double quotes:

:contains('hello')
:contains("hello")

Type inheritance matching

:isa()

Matches elements whose itemtype is the target URL or any descendant type per the schema inheritance hierarchy.

:isa('itemtype-url')

This pseudo-class is schema-aware: it consults the inheritance hierarchy defined by your schemas to determine whether an element's type is a descendant of the target type. The match is reflexive -- an element whose itemtype exactly equals the target also matches.

Selector Matches
:isa('https://schema.org/Thing') Elements with itemtype="https://schema.org/Thing" or any schema-defined descendant (e.g., Person, Organization)
div:isa('https://example.com/Base') <div> elements whose itemtype is Base or any descendant of Base

Without a schema inheritance hierarchy (e.g., when no schemas are loaded), :isa() matches nothing. Reflexive matching requires the inheritance map to be present.

:isa() is particularly useful for polymorphic queries -- finding all elements of a given type family without listing every descendant type explicitly:

/* Instead of listing every authorization rule type: */
[itemtype='https://pagelove.org/AuthorizationRule'],
[itemtype='https://pagelove.org/PathAuthorizationRule'],
[itemtype='https://pagelove.org/TypeAuthorizationRule']

/* Use :isa() for a single polymorphic query: */
:isa('https://pagelove.org/AuthorizationRule')

The query planner recognizes :isa() and emits itemtype index lookups for the target and all descendants, so polymorphic queries benefit from the same index acceleration as explicit [itemtype] selectors.

See also

Non-ASCII text in selectors

A selector sent in the Range header may contain non-ASCII text directly — the platform decodes header values as UTF-8, so p:contains('café') works as written. If your client or an intermediary mangles non-ASCII header bytes, use CSS escape sequences instead (p:contains('caf\E9 ') — note the trailing space terminating the escape).

Method Elements

Method Elements

A method element is an HTML element whose tag is a Schema-defined method name in a bound XML namespace. At composition time the element is replaced with the result of evaluating the method's implementation. The implementation is written in Sessel or in JavaScript (a JavaScript/Module); both produce the same kinds of result (see How the result becomes HTML) and differ only in how they receive self/this and their arguments.

When to use

Reach for a method element when the same fragment of HTML is produced by Sessel logic you'd rather declare once on a Schema than repeat inline in every page that needs it. Common shapes:

If the snippet is purely declarative (a fragment of another document with no logic), use Includes instead. If it's a Liquid template parameterised by a Resource Binding, use Templating.

Element form

<html xmlns:t="urn:Test">
  <body>
    <t:foo></t:foo>
  </body>
</html>

Two ingredients:

  1. An xmlns declaration binding a prefix (t) to a schema URL (urn:Test).
  2. An element whose tag is prefix:method-name (t:foo).

The xmlns declaration may live on <html>, <body>, or any ancestor of the element you want to dispatch. The binding is scoped to its declaring element's subtree.

Attribute form

A method can also be dispatched from a prefix:name="value" attribute on an ordinary (non-prefixed) element, rather than from a prefixed tag:

<html xmlns:t="urn:Test">
  <body>
    <div t:foo="bar"></div>
  </body>
</html>

The same two ingredients apply — an xmlns declaration binding prefix to a schema, and name matching a declared method on that schema — but the dispatch target is the attribute, not the element's tag. Differences from the element form:

Multiple prefix:name="value" attributes on the same element (whether from the same or different bound prefixes) are not dispatched in the order they're written. Any *:template attribute is always dispatched last, after every other prefixed attribute on the element, regardless of where it appears in the markup — this lets p:template read Context bindings that other attributes on the same element just wrote. Among the rest, dispatch is one after another — unless one of them replaces the host element. As soon as a dispatched method whose declared returns type is https://pagelove.org/Element yields a non-Null result — triggering the whole-host-element replacement described above, regardless of what kind of value the result actually is — dispatch of that element's remaining attributes stops: any attribute still waiting its turn never runs, including a side-effect-only one (a Context mutation with no element return), and this applies to *:template too if it hasn't run yet. If you need every attribute's side effects to run, don't combine a replacing method with other prefixed attributes on the same element.

This attribute-form dispatch is also how the built-in Resource Binding (r:), Expression Binding (e:), and JavaScript expression binding (j:) namespaces work under the hood: each declares a schema whose doesNotUnderstand method is dispatched through this same generic path, with the attribute value arriving as parameters[0] (see doesNotUnderstand fallback). Those pages describe the binding-specific behaviour; this page describes the general dispatch mechanism they specialize.

Defining the method

A method element requires a modeled schema — it is the composition feature a schema's Methods unlock. (Composition mechanisms that need no schema — templates, resource and expression bindings, includes — are listed under Composing pages.)

The schema bound to the prefix must declare a Method whose name matches the element's local name, and must be loaded into the host's cache (registered when the document containing the schema is PUT/POST'd; until then dispatch fails — see Error cases). In brief, a Method is a <li itemprop="property" itemscope itemtype="https://pagelove.org/Method"> carrying a name, an implementation (Sessel or JavaScript), an optional returns type, and optional parameter items. See Methods for the full declaration reference; the example below shows one inline, and JavaScript implementations covers the JavaScript calling convention.

How the result becomes HTML

The implementation is evaluated with the dispatched element bound as self (Sessel) or this (JavaScript). The returned value determines how the document tree is updated, the same way for both languages:

Return type Result
Element (a constructed element such as new ul { ... }) Serialised as HTML, parsed as a fragment, spliced in place of the method element. Composition recurses into the spliced fragment.
Instance of a Schema Serialised as the instance's microdata, spliced in place. Composition recurses.
List of Element/Instance Each item serialised, all spliced in document order.
Scalar (String, Integer, Boolean, etc.) Stringified via the standard value_to_html rules and inserted as text.
Null Element is removed from the tree.

JavaScript implementations

An implementation whose itemtype is https://pagelove.org/JavaScript/Module carries an ES module in its source. The module's default export is the method:

<div itemprop="implementation" itemscope itemtype="https://pagelove.org/JavaScript/Module">
  <script itemprop="source" type="module">
    export default () => {
      const d = new DOMParser().parseFromString("<p>hello</p>", "text/html");
      return d.querySelector("p");
    };
  </script>
</div>

When such a method is dispatched in the composition pipeline:

The source may be an async function; the pipeline drives its returned promise to settlement.

For declaring JavaScript bindings inside Schema HTML in general (defaults, @read, @computed, methods), see Schema definitions in HTML.

JavaScript example

A method whose JavaScript implementation builds and returns a <p>. As with the Sessel example below, the schema is inline so a single PUT registers it.

PUT /js-method-element-demo.html HTTP/2
Host: 127.0.0.1
Content-Type: text/html

<!DOCTYPE html>
<html xmlns:j="urn:JsDemo">
  <body>
    <main>
      <j:hello></j:hello>
    </main>
    <div hidden itemscope itemtype="https://pagelove.org/AuthorizationRule">
      <meta itemprop="actor" content="*">
      <meta itemprop="resource" content="/*">
      <meta itemprop="method" content="GET">
      <meta itemprop="action" content="allow">
    </div>
    <div hidden itemscope itemtype="https://pagelove.org/Schema">
      <meta itemprop="type" content="urn:JsDemo">
      <ul>
        <li itemprop="property" itemscope itemtype="https://pagelove.org/Method">
          <meta itemprop="name" content="hello">
          <meta itemprop="returns" content="https://pagelove.org/Element">
          <div itemprop="implementation" itemscope itemtype="https://pagelove.org/JavaScript/Module">
            <script itemprop="source" type="module">export default () => { const d = new DOMParser().parseFromString("<p>hello from JavaScript</p>", "text/html"); return d.querySelector("p"); };</script>
          </div>
        </li>
      </ul>
    </div>
  </body>
</html>
GET /js-method-element-demo.html HTTP/2
Host: 127.0.0.1
Range: selector=main
HTTP/2 206
content-range: selector main

hello from JavaScript

Passing arguments

Attributes on the method element become arguments to the method — named local variables in Sessel, positional arguments in JavaScript:

<t:greet name="Ada"></t:greet>

The matching schema:

<li itemprop="property" itemscope itemtype="https://pagelove.org/Method">
  <meta itemprop="name" content="greet">
  <meta itemprop="returns" content="https://pagelove.org/Element">
  <li itemprop="parameter" itemscope itemtype="https://pagelove.org/Parameter">
    <meta itemprop="name" content="name">
    <meta itemprop="type" content="https://schema.host/Text">
  </li>
  <div itemprop="implementation" itemscope itemtype="https://pagelove.org/Sessel">
    <script itemprop="source" type="text/sessel">
      new p { "Hello, " + name + "!" }
    </script>
  </div>
</li>

The dispatcher matches attribute names against declared parameter names. Attributes whose names don't match a parameter (and aren't xmlns declarations) are ignored.

When two methods share a name but declare different parameters, the dispatcher picks the overload whose declared parameters all appear as attributes — the most specific match wins.

doesNotUnderstand fallback

If a schema declares a method literally named doesNotUnderstand, it catches every unmatched dispatch under bound prefixes for that schema — from either invocation form. It always receives messageName (the unrecognised local name), but the shape of parameters differs by the form that triggered the dispatch:

Invocation form parameters shape
Element form (<t:foo attr="v">) A list of { name, value } maps, one per attribute on the element whose name doesn't begin with xmlns: — note this excludes a prefixed xmlns: declaration but not a bare xmlns="…" attribute, which is passed through like any other.
Attribute form (<div t:foo="v">) A list containing a single bare string — the dispatching attribute's value.

A Sessel doesNotUnderstand reads messageName and parameters as named locals; a JavaScript doesNotUnderstand declares them as parameters (messageName, then parameters) and receives them positionally. In both languages, parameters itself is shaped per the table above — code that reads parameters[0] gets a bare value under the attribute form but a { name, value } map under the element form.

This is the only escape hatch when you want a single method to handle many element or attribute names — for example, a <t:any-tag>-style passthrough, or (as the built-in Binding/CSS schema does) a css:* attribute whose value is evaluated as a CSS selector regardless of its local name. Without it, the two invocation forms fail differently for an undeclared name under a bound prefix: the element form fails composition with an HTTP 500 (see Error cases); the attribute form fails silently — the attribute is simply stripped from the output with no dispatch and no error.

Attribute-form example

The built-in Binding/CSS schema — the mechanism behind Resource Binding's r: attributes — is itself just a doesNotUnderstand method reached through this attribute-form dispatch:

<li itemprop="property" itemscope itemtype="https://pagelove.org/Method">
  <meta itemprop="name" content="doesNotUnderstand">
  <div itemprop="implementation" itemscope itemtype="https://pagelove.org/Sessel">
    <script itemprop="source" type="text/sessel">
      @schema Selector url("https://pagelove.org/Selector");
      let result = new Selector { selector: parameters[0] }.execute();
      Context[messageName] = result;
      result
    </script>
  </div>
</li>

An r:items="li" attribute has no declared items method, so it dispatches to this doesNotUnderstand: messageName is "items" and parameters is ["li"] (the bare-string shape from the table above). The method evaluates parameters[0] as a CSS selector and binds the result into Context.items, which a sibling p:template — dispatched after it, per the ordering rule above — can then render. See Resource Binding for the full worked example.

End-to-end testable example. A minimal urn:AttrDemo schema declares only doesNotUnderstand, so any t:*="…" attribute other than the reserved t:transient marker reaches it (see the transient exception above); the schema is provided inline so a single PUT registers both it and the page.

PUT /attr-form-demo.html HTTP/2
Host: 127.0.0.1
Content-Type: text/html

<!DOCTYPE html>
<html xmlns:t="urn:AttrDemo">
  <body>
    <main>
      <div t:greet="Ada"></div>
    </main>
    <div hidden itemscope itemtype="https://pagelove.org/AuthorizationRule">
      <meta itemprop="actor" content="*">
      <meta itemprop="resource" content="/*">
      <meta itemprop="method" content="GET">
      <meta itemprop="action" content="allow">
    </div>
    <div hidden itemscope itemtype="https://pagelove.org/Schema">
      <meta itemprop="type" content="urn:AttrDemo">
      <ul>
        <li itemprop="property" itemscope itemtype="https://pagelove.org/Method">
          <meta itemprop="name" content="doesNotUnderstand">
          <meta itemprop="returns" content="https://pagelove.org/Element">
          <div itemprop="implementation" itemscope itemtype="https://pagelove.org/Sessel">
            <script itemprop="source" type="text/sessel">
              new p { messageName + ": " + parameters[0] }
            </script>
          </div>
        </li>
      </ul>
    </div>
  </body>
</html>
GET /attr-form-demo.html HTTP/2
Host: 127.0.0.1
Range: selector=main
HTTP/2 206
content-range: selector main

greet: Ada

The <div t:greet="Ada"> has no declared greet method on urn:AttrDemo, so it dispatches to doesNotUnderstand: messageName is "greet" and parameters is ["Ada"] — the bare-string shape from the table above, not a { name, value } map. Because the method's returns type is https://pagelove.org/Element and the result isn't Null, the whole <div> — not just the t:greet attribute — is replaced by the returned <p>greet: Ada</p>, per the whole-host-element replacement rule described above.

Error cases

The two forms fail differently when a prefix is unbound or a name is undeclared — the element form always errors, the attribute form never does. Declaring doesNotUnderstand on the schema (see doesNotUnderstand fallback) catches an undeclared name under a bound prefix identically in both forms; it cannot help with an unbound prefix, since the prefix itself isn't recognised in either form.

Condition Form Result
The xmlns prefix is unbound (e.g. typo: xmlsns:t="urn:Test") Element The element's prefix doesn't appear in the dispatch table. HTTP 500: no method found for <prefix>:<name> and no doesNotUnderstand defined.
The xmlns prefix is unbound Attribute No dispatch is attempted and no error is raised — the attribute is left in the output exactly as written.
The schema isn't loaded into the host's cache, or declares no matching method and no doesNotUnderstand Element Same HTTP 500 as an unbound prefix — the method exists in the page's inline schema but the cache hasn't been refreshed, or the name is genuinely undeclared. PUT the document containing the schema once to register it.
The schema isn't loaded into the host's cache, or declares no matching method and no doesNotUnderstand Attribute No error. The attribute is silently stripped from the output with no dispatch — see doesNotUnderstand fallback.
The method has no implementation Both The dispatcher invokes a method whose source is empty; the result is Null and the element (element form) or dispatching attribute (attribute form) is removed.
The implementation raises an error (a Sessel error, or a thrown JavaScript error such as a NoModificationAllowedError from mutating the read-only document) Both The composition fails with HTTP 500 and the error message.
Composition budget exhausted (500 dispatches per request) Both HTTP 503 composition budget exceeded. Each method element or dispatched attribute costs one budget unit; recursive results cost more.

Example

End-to-end testable example. The schema is provided inline in the test page so a single PUT registers both the schema and the page.

PUT /method-element-demo.html HTTP/2
Host: 127.0.0.1
Content-Type: text/html

<!DOCTYPE html>
<html xmlns:t="urn:Demo">
  <body>
    <main>
      <t:hello></t:hello>
    </main>
    <div hidden itemscope itemtype="https://pagelove.org/AuthorizationRule">
      <meta itemprop="actor" content="*">
      <meta itemprop="resource" content="/*">
      <meta itemprop="method" content="GET">
      <meta itemprop="action" content="allow">
    </div>
    <div hidden itemscope itemtype="https://pagelove.org/Schema">
      <meta itemprop="type" content="urn:Demo">
      <ul>
        <li itemprop="property" itemscope itemtype="https://pagelove.org/Method">
          <meta itemprop="name" content="hello">
          <meta itemprop="returns" content="https://pagelove.org/Element">
          <div itemprop="implementation" itemscope itemtype="https://pagelove.org/Sessel">
            <script itemprop="source" type="text/sessel">
              new p { "hello from urn:Demo" }
            </script>
          </div>
        </li>
      </ul>
    </div>
  </body>
</html>
GET /method-element-demo.html HTTP/2
Host: 127.0.0.1
Range: selector=main
HTTP/2 206
content-range: selector main

hello from urn:Demo

The <t:hello> is replaced by the method's returned <p> fragment. The Range: selector=main reads only the <main> subtree to keep the assertion focused on the dispatched output.

See also

Stamp

Stamp

<p:stamp> emits the value of one or more Context entries into the document at the point where it sits. It is the mechanism that places a computed value — most often the result of an Expression Binding — into the rendered page as HTML.

stamp is a built-in Method on the Pagelove namespace (https://pagelove.org/1.0); <p:stamp> is a Method Element that invokes it. Like an Include, a stamped value keeps its origin identity, so it can be written through with PUT, POST, and DELETE — see Interaction with HTTP Document Mutation.

When to use

A binding computes a value and exposes it under a name in Context, but a binding by itself does not render anything. Reach for <p:stamp> to surface that value where you want it to appear. Common shapes:

To render a list of items one element each, use Templating instead; stamp emits the named values as-is.

Form

<html xmlns:p="https://pagelove.org/1.0">
  <body>
    <p:stamp greeting></p:stamp>
  </body>
</html>

Two ingredients:

  1. An xmlns declaration binding a prefix (p) to the Pagelove namespace https://pagelove.org/1.0.
  2. A <p:stamp> element whose attribute names are the Context keys to emit. <p:stamp greeting> emits Context.greeting; <p:stamp greeting subtitle> emits both. The attribute values are ignored — only the names matter.

The xmlns declaration may live on <html>, <body>, or any ancestor of the <p:stamp> element.

How a value reaches Context

stamp reads Context[name] for each named attribute. A value gets into Context during composition by:

A <p:stamp> must appear after whatever wrote the value it names, in composition (tree-walk) order.

How the result is emitted

For the named keys, stamp collects each non-null Context value and returns:

Named values present Result
None (all null/absent) null — the <p:stamp> element is removed from the tree.
Exactly one That value.
More than one A list of the values, in attribute order.

Because the stamp method declares returns https://pagelove.org/Element, the returned value is handled by the method-element result rules: an element (or list of elements) is spliced in place of <p:stamp> and composition recurses into it; a scalar is inserted as text.

Routing a stamped instance

When a stamped value is a schema instance whose schema declares a @key property, the key is emitted as the spliced root element's id, so the instance remains addressable by #<key-value> inside the containing document.

Interaction with HTTP Document Mutation (PUT, POST & DELETE)

Like an Include, a stamped fragment retains its origin identity — the resource and element it was stamped from.

When a client performs a mutating HTTP request (PUT, POST, or DELETE) targeting an element that originated from a <p:stamp>:

For example, a DELETE addressed at a record stamped under <p:stamp currentuser> deletes the origin record, not the page that stamped it. This makes a stamped value a first-class, HTTP-addressable view of its source, not a render-time snapshot.

This write-through also applies when the stamping page is served from a parameterized route: a selector write to a stamped element on a concrete route URL (e.g. POST a comment to /posts/hello-world.html, where the page is the template /posts/:slug.html) resolves the route, composes the stamped record, and routes the write to its origin — just as for a literal page. Authorization is checked on the concrete route page the request is addressed to, not the origin data file.

Error cases

Condition Result
The p prefix is unbound, or bound to a URI other than https://pagelove.org/1.0 <p:stamp> is not recognised as a method element; dispatch fails (see Method Elements).
A named key has no Context value That key contributes nothing; if every named key is absent, the element is removed.
A namespace other than stamp's is invoked on the Pagelove prefix (e.g. <p:unknown>) HTTP 500: unknown method '<name>' on Pagelove namespace.

Example

A page binds a value with an Expression Binding and stamps it into <main>. The schema for stamp is built in, so no schema declaration is needed.

PUT /stamp-demo.html HTTP/2
Host: 127.0.0.1
Content-Type: text/html

<!DOCTYPE html>
<html xmlns:e="https://pagelove.org/Binding/Sessel" xmlns:p="https://pagelove.org/1.0">
  <body e:greeting="'hello from a binding'">
    <main>
      <p:stamp greeting></p:stamp>
    </main>
    <div hidden itemscope itemtype="https://pagelove.org/AuthorizationRule">
      <meta itemprop="actor" content="*">
      <meta itemprop="resource" content="/*">
      <meta itemprop="method" content="GET">
      <meta itemprop="action" content="allow">
    </div>
  </body>
</html>
GET /stamp-demo.html HTTP/2
Host: 127.0.0.1
Range: selector=main
HTTP/2 206
content-range: selector main

hello from a binding

The e:greeting binding writes Context.greeting; <p:stamp greeting> reads it and is replaced by its value.

See also

XML documents

XML documents

A document stored with an XML-family content type is composed with the same namespaced directives as HTML, and served back as well-formed XML. This makes RSS and Atom feeds, sitemaps, SVG images, and XHTML dynamic without leaving the XML serialization.

XML-family content types

A document is treated as XML when its stored content type is application/xml, text/xml, or any …+xml structured-syntax-suffix type:

Content type Typical use
application/xml, text/xml Generic XML
application/rss+xml RSS feeds
application/atom+xml Atom feeds
image/svg+xml SVG images
application/xhtml+xml XHTML
any other …+xml type sitemaps, and so on

The content type is set when the document is written — send it as the Content-Type header on PUT, or let it be inferred from the file extension. The response keeps the stored XML content type.

The XML dialect

XML documents are parsed with an XML dialect rather than the lenient HTML parser:

The composed result is serialised as well-formed XML: empty elements self-close (<entry/>), element and attribute case is preserved, and processing instructions are retained.

Composition

XML documents run through the same composition pipeline as HTML. Every namespaced directive works:

Microdata @read resolvers also run on XML: an element carrying itemscope/itemprop whose schema declares a @read resolver has its values transformed on the read path, exactly as in HTML. An XML document with no Pagelove namespaces and no microdata is served unchanged.

Differences from HTML composition

Two HTML-representation behaviours do not apply to XML documents:

Behaviour HTML XML
JSON-LD content negotiation Accept: application/ld+json returns JSON-LD Not applied — an XML document is always served as XML, regardless of Accept
xmlns: declaration stripping Pagelove xmlns: declarations are removed from the response Preserved — an XML document legitimately declares namespaces, so all xmlns: declarations (including xmlns:e / xmlns:p) remain in the served output

Example

An Atom feed whose <title> is filled by an Expression Binding and emitted with <p:stamp>. It is stored with an XML content type and composed on read:

PUT /feed.xml HTTP/2
Host: 127.0.0.1
Content-Type: application/atom+xml

<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:e="https://pagelove.org/Binding/Sessel" xmlns:p="https://pagelove.org/1.0" e:site="'Pagelove Blog'">
  <title><p:stamp site></p:stamp></title>
  <entry><id>urn:1</id></entry>
  <div hidden="hidden" itemscope="itemscope" itemtype="https://pagelove.org/AuthorizationRule">
    <meta itemprop="actor" content="*"></meta>
    <meta itemprop="resource" content="/*"></meta>
    <meta itemprop="method" content="GET"></meta>
    <meta itemprop="action" content="allow"></meta>
  </div>
</feed>

Fetching the feed runs composition and returns well-formed XML with its stored content type:

GET /feed.xml HTTP/2
Host: 127.0.0.1
HTTP/2 200
content-type: application/atom+xml

<title>Pagelove Blog</title>

The e:site binding resolves and <p:stamp site> is replaced by its value; the xmlns: declarations remain because stripping is HTML-only.

See also

Includes

Includes

<p:include> is a declarative HTML element that includes a fragment of another document (or of the site as a whole) into the current page, resolved by a CSS selector at request time.

Unlike traditional template partials, included fragments remain addressable, authorisable, and writable through Pagelove’s HTTP and selector‑based model.

Namespace declaration

include lives in the Pagelove server-side-processing namespace https://pagelove.org/1.0. Declare it on an ancestor element (usually <html>); the prefix can be any valid XML prefix, but p is conventional and is used throughout these docs:

<html xmlns:p="https://pagelove.org/1.0">

The element-form snippets below show only the <p:include> element itself; each assumes this declaration is present on an ancestor.

Element form

<p:include
  selector="..."
  resource="..." />

Attributes:

Attribute semantics

selector (required)

A CSS selector identifying the fragment to include.

The selector is evaluated against a set of candidate resources determined by the resource attribute (if present) or the entire site graph (if not).

resource (optional)

Constrains the search space for the selector.

When present, only resources whose path matches the value(s) of resource are searched. Glob-style (*, ** & ?)wildcards are supported

When omitted, the selector is evaluated across the entire site graph.

resource does not mean “include the whole resource”. A selector is always required.

This is invalid:

<p:include resource="/partials.html" />

This is valid:

<p:include resource="/partials.html" selector="header#nav" />

Resolution model

Given:

<p:include selector="S" resource="R?" />

The server resolves the include as follows:

  1. Determine the candidate resource set:
    • If resource is present: all resources matching resource.
    • Otherwise: the entire site graph.
  2. Evaluate selector within each candidate resource.
  3. Collect all matching elements across all candidates.
  4. Apply cardinality rules (below).
  5. If resolution succeeds, materialise the matched element in place of the <p:include> node, retaining a link to the origin resource and element so that mutations write through to the source.

Cardinality rules

<p:include> is defined to resolve to exactly one element.

The following rules are enforced:

404 Not Found
500 Internal Server Error

Multiple matches are treated as a site integrity failure, not a client input error. An include is expected to identify a single canonical fragment. Ambiguity indicates incorrect site composition.

Examples

Global selector include

Searches the entire site graph.

<p:include selector="#partials header#nav" />

Constrained include

Searches only within /partials.html.

<p:include
  resource="/partials.html"
  selector="header#nav" />

Basic include resolves a fragment from another document

Store a partial containing a navigation header, then a page that includes it via <p:include>:

<!DOCTYPE html>
<html xmlns:p="https://pagelove.org/1.0">
<body>
  <p:include selector="#nav" resource="/sspi-inc-partials/*" />
  <main><p>Page content here.</p></main>
</body>
</html>

When the page is requested, the include is resolved and the fragment is inlined:

GET /sspi-inc-basic.html
HTTP/1.1 200

<!DOCTYPE html>
<html>
<body>
  <header id="nav">Navigation</header>
  <main><p>Page content here.</p></main>
</body>
</html>

The <p:include> element is gone, replaced by the actual <header> from the partial. xmlns:p — like every xmlns:* declaration in the composed document — has been stripped.

Interaction with HTTP Document Mutation (PUT, POST & DELETE)

Included fragments retain their origin identity.

When a client performs a mutating HTTP request (such as PUT) targeting an element that originated from an included fragment:

If an include fails to resolve uniquely, no write is attempted.

Summary

<p:include> provides deterministic, selector‑based document composition across the site graph.

It:

This makes fragment inclusion a first‑class, HTTP‑addressable primitive rather than a template‑time convenience.

See also

Transient Elements

Transient Elements

The transient attribute marks an element as session-scoped. The element's content in the document serves as the default for new sessions. Each session sees its own version, and changes do not affect other sessions or the canonical document.

Attribute form

<ul id="cart" p:transient>
    <li>Default item</li>
</ul>

p:transient takes no value. It can be applied to any element within a Pagelove namespace.

Reading transient elements

When a document containing transient elements is requested:

Transient resolution is per‑session and per‑request — it is never cached across sessions. Every response built from a document that carries a transient marker is served Cache-Control: private: the full document and any selector‑range read of it (whichever element the selector matches), with or without a session. Shared caches therefore never store one visitor's transient content and replay it to another.

Transient element content is not visible to Resource Binding. Bindings query the canonical document, not session‑scoped content. To use transient data elsewhere, the client must read it from the DOM and transfer it explicitly.

Mutating transient elements

PUT and POST

A PUT or POST targeting a transient element (or any child of one) writes to the session rather than the document.

PUT /page.html #cart

The new content must preserve the element's identity — it must still match the CSS selector that identifies it. If it does not, the request is rejected:

422 Unprocessable Entity

For example, given <ul id="cart" p:transient>:

The transient attribute does not need to be included in the request body; it is preserved automatically.

DELETE

A DELETE targeting a transient element removes the session content, reverting to the document default on the next request.

DELETE /page.html #cart

Child mutations

When a mutation targets a child of a transient element, it is treated as a mutation of the transient ancestor.

<ul id="cart" p:transient>
    <li id="item1">Apples</li>
</ul>

A PUT to #item1 is handled as a transient mutation of #cart, because #cart is the transient ancestor.

Default behaviour

When no session content exists for a transient element, the directive is inert — the document default is served. This applies to:

Transient content expires 30 days after it was written, regardless of the session's own configured lifetime — the two are independent.

Sessions and access control

Every visitor has a session, including unauthenticated users: the edge proxy establishes a pagelove_session cookie automatically on the first request and replays it thereafter. This means unauthenticated users can modify transient elements unless access control rules prevent it.

Use AuthorizationRule declarations to control who may read or write transient elements, just as with any other Pagelove content.

Error responses

Status Condition
409 Conflict A mutation reached the server with no session established. On the public web the edge proxy always establishes the pagelove_session cookie first, so this is not normally reachable.
405 Method Not Allowed Unsupported HTTP method for a transient element.
422 Unprocessable Entity PUT content does not match the element's CSS selector.

Future extensibility

The attribute value is reserved for a per‑element TTL in seconds:

<ul id="cart" p:transient="3600">
    <li>Expires in 1 hour</li>
</ul>

Currently, this value is ignored: every transient write uses a fixed 30-day TTL regardless of what's specified here. Per‑element TTL support is planned for a future release.

Example: default content for a new session

Store a page with a transient element containing default content:

<!DOCTYPE html>
<html xmlns:p="https://pagelove.org/1.0">
<body>
  <ul id="cart" p:transient>
    <li>Default item</li>
  </ul>
</body>
</html>

A fresh session receives the document default:

GET /sspi-transient-page.html
HTTP/1.1 200

<!DOCTYPE html>
<html>
<body>
  <ul id="cart">
    <li>Default item</li>
  </ul>
</body>
</html>

Summary

p:transient provides session‑scoped element content within Pagelove's declarative document model.

It:

See also

Parameterized routes

Parameterized routes

A document stored at a path containing :name segments is a route template. When a whole-document GET finds no literal document at the request path, the server matches the path against these templates, captures the concrete segment values as named parameters, and exposes them to composition as request.params.

When to reach for it

Use a parameterized route to serve many URLs from one stored document — /users/42/profile.html, /users/alice/profile.html, and so on all rendered by a single template stored at /users/:id/profile.html. The captured value drives composition (expression bindings, templates, schema methods), so each URL renders its own content from the one template.

Authoring a route

Store (PUT) the template document at a path whose segments begin with :. The colon is a literal character in the stored path. Directory segments, the filename, or both may be parameterized:

Stored path Matches a request like Captures
/users/:id/profile.html /users/42/profile.html id = "42"
/orgs/:org_id/teams/:team_id/members.html /orgs/acme/teams/backend/members.html org_id = "acme", team_id = "backend"
/pages/:slug.html /pages/hello-world.html slug = "hello-world"

A parameterized filename captures the request filename up to a matching extension: :slug.html against /pages/hello.html captures slug = "hello", and the request filename must end in .html. A bare :slug with no extension captures the entire request filename.

Resolution

Parameterized resolution runs only when both hold:

  1. The request is a whole-document GET — no selector (Range) is present, and
  2. No literal document exists at the request path.

The resolver then walks the stored directory tree segment by segment. For each request segment it matches a literal child name first, then any :param child. Captured values are percent-decoded/pages/hello%20world.html captures slug = "hello world".

Whole-document writes (PUT, POST, DELETE, MOVE with no Range selector) are literal only: they do not resolve parameterized routes. A whole-document write addresses the stored path verbatim, so PUT /users/:id/profile.html edits the template document itself.

A selector write (Range: selector=…) to a parameterized-route URL is resolved differently, to match the behaviour of a literal composed page: the route is resolved, the template composed (with request.params available), and — if the selector targets a stamped or included element (<p:stamp>/<p:include>) — the write routes through to that element's origin resource, exactly as it would on a literal composed page. This is what lets, e.g., a comment POSTed to a /posts/:slug.html page reach the post's origin data file. Authorization is evaluated against the composed route page (the concrete URL the request addresses), not the origin resource — so an AuthorizationRule permitting the write on the route page is sufficient, and the origin needs no rule of its own. A selector write whose selector matches no element in the composed route page (or matches an element that is not stamped/included, so it has no writable origin) returns 416 Range Not Satisfiable — exactly as a selector-no-match does on a literal composed page. The route template is never written through the concrete URL. (A concrete path that matches no route at all still returns 404.)

Most-literal-wins

When more than one template matches a request, the candidate with the most literal (non-:param) segments wins. Ties are broken by the stored template path, compared lexicographically in ascending order.

Request Matching templates Winner
/pages/about/index.html /pages/about/index.html, /pages/:slug/index.html /pages/about/index.html — more literal segments
/items/x/view.html /items/:a_param/view.html, /items/:b_param/view.html /items/:a_param/view.html — lexicographically first

A literal document stored at the exact request path always wins, because the literal lookup is performed before parameterized resolution is attempted.

Reading captured parameters

Captured parameters are exposed to composition under request.params:

request.params is a shared request member: distinct URLs are distinct cache keys, so a parameterized page remains publicly cacheable per concrete URL.

Example

One template document renders every /users/<id>/profile.html URL. It binds the captured id with an Expression Binding and emits it with <p:stamp>. The template is stored once at the :id path:

PUT /users/:id/profile.html HTTP/2
Host: 127.0.0.1
Content-Type: text/html

<!DOCTYPE html>
<html xmlns:e="https://pagelove.org/Binding/Sessel" xmlns:p="https://pagelove.org/1.0">
  <body e:uid="request.params.id">
    <main><p:stamp uid></p:stamp></main>
    <div hidden itemscope itemtype="https://pagelove.org/AuthorizationRule">
      <meta itemprop="actor" content="*">
      <meta itemprop="resource" content="/*">
      <meta itemprop="method" content="GET">
      <meta itemprop="action" content="allow">
    </div>
  </body>
</html>

A concrete URL resolves to that template, with id captured from the path:

GET /users/42/profile.html HTTP/2
Host: 127.0.0.1
HTTP/2 200

<main>42</main>

/users/alice/profile.html renders <main>alice</main> from the same stored document.

Error cases

Condition Result
No literal document and no template matches 404 Not Found
Directory segments match a template but the final document does not exist 404 Not Found — there is no partial match
Selector (Range) GET to a path with no literal document 404 Not Found — parameterized routes are resolved only for whole-document reads

See also