Fruitful Docs
GuidesRender

Build the Presentation View

Turn the root record and its related records into the View your surfaces read.

Activity does not render records directly. The package builds a View, a Lexicon-validated value shaped for presentation, and the surfaces bind to it. The View is built deterministically by buildView and persisted with the exact resolution digest, so what a person sees is reproducible from what was captured.

Declare the render block

"render": {
  "root": "app.fruitful.feed.hackerNews.submission",
  "view": "app.fruitful.feed.hackerNews.defs@^1.0.0#submissionView",
  "occurredAt": "/submittedAt",
  "headline": { "path": "/title", "default": "Hacker News submission" },
  "surfaces": { "compact": "presentation/compact.surface.json", "expanded": "presentation/reader.surface.json" },
  "actions": [
    { "key": "article-reader", "label": "Article", "view": "fruitful.webReader.v1", "url": "/record/destinationUrl", "mediaType": "text/html" },
    { "key": "hacker-news-browser", "label": "Hacker News", "view": "fruitful.browser.v1", "url": "/record/uri", "rel": ["canonical", "replies"] }
  ]
}
  • root is the record type that becomes an Activity item. It must be in transform.records.
  • view is the View def buildView must produce.
  • occurredAt and headline feed the Activity shell. Both fall back: capture time, and headline.default.
  • actions are the other ways to open the item. Website is never automatic; it is an action the View declares with a valid URL.

Define the View Lexicon

The View is a def in the package's defs Lexicon. It contains what the surfaces need and nothing else:

"submissionView": {
  "type": "object",
  "required": ["record"],
  "properties": {
    "record": { "type": "ref", "ref": "app.fruitful.feed.hackerNews.submission" },
    "submitter": { "type": "ref", "ref": "#submitterView" }
  }
}

Build it

buildView receives the handler name, the root record, and every record the transform emitted alongside it:

buildView({ handler, root, records }) {
  if (handler !== 'submissionView' || root.$type !== Submission.$type) {
    throw new Error(`Unsupported handler ${handler}`);
  }
  const submission = Submission.$parse(root);
  const account = records
    .map((record) => Account.$safeParse(record))
    .find((result) => result.success && result.value.uri === submission.submitter?.uri);
  return SubmissionView.$build({
    record: submission,
    ...(account?.success ? { submitter: { username: account.value.providerIdentity.username } } : {}),
  });
}

Use the generated builders; a View that fails validation fails closed as Presentation unavailable in the product, never as a Website fallback. Keep it deterministic: same records in, same bytes out. Validation runs the transform twice and compares digests.

Reference: render, headline, action.

On this page