Transforming data on write and read

This recipe shows how to rewrite a property's value as it enters or leaves the server using @write and @read resolvers on a schema property.

When to use this approach

Use @write when a stored value should be normalized before it hits storage — lowercasing a slug, trimming whitespace off a title, computing a derived value from one or more sibling properties. The resolver runs on the server, at write time, and what it returns is what ends up on the article.

Use @read when the stored value should be transformed before it reaches the caller — formatting a timestamp, joining a list into a presentation-friendly string, hiding a sensitive suffix. The resolver runs at read time, and what it returns is what the caller sees. The stored value is unchanged.

Do not use these for business-logic validation — that is what @validate expressions are for. A resolver that wants to reject bad input should let a validator catch it on the next pass.

Lowercasing a slug on write

The slug property of a Project schema should always be stored in lowercase. A @write resolver handles the normalisation inside the property declaration:

<li itemprop="property" itemscope itemtype="https://pagelove.org/Property">
  <meta itemprop="name" content="slug">
  <meta itemprop="type" content="https://schema.host/Text">
  <meta itemprop="cardinality" content="1..1">
  <script type="text/sessel" itemprop="@write">
    self.map((el) => {
      let normalized = el.value().lowercase();
      new meta[itemprop="slug"][content=normalized] {}
    })
  </script>
</li>

self is the list of every [itemprop="slug"] element in the incoming write — for a 1..1 property, a one-element list. The expression maps each element to a new <meta> element whose content is the lowercase form of the original value. The replacements go into storage in place of the originals.

A PUT carrying content="My-Project" is stored as content="my-project". A subsequent GET returns the lowercase form — the original casing is gone.

Formatting a date on read

The opposite direction: the stored value is a raw ISO 8601 timestamp, and reads should return a friendlier format. A @read resolver handles the transform on the way out:

<li itemprop="property" itemscope itemtype="https://pagelove.org/Property">
  <meta itemprop="name" content="createdAt">
  <meta itemprop="type" content="https://schema.host/DateTime">
  <meta itemprop="cardinality" content="1..1">
  <script type="text/sessel" itemprop="@read">
    self.map((el) => {
      let stamp = el.value().parseDateTime();
      new time[itemprop="createdAt"][datetime=el.value()] {
        stamp.format("d MMMM yyyy")
      }
    })
  </script>
</li>

The stored <time itemprop="createdAt" datetime="2026-04-08T09:15:00Z"> round-trips through the resolver and comes out as <time itemprop="createdAt" datetime="2026-04-08T09:15:00Z">8 April 2026</time>. The datetime attribute keeps the machine-readable ISO form; the visible text is the formatted version.

Choosing between server-side and client-side

Both @write and @read can also be expressed as client-side JavaScript modules — see Schema definitions in HTML for the embedding syntax. The short version:

See also