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"] }
]
}rootis the record type that becomes an Activity item. It must be intransform.records.viewis the View defbuildViewmust produce.occurredAtandheadlinefeed the Activity shell. Both fall back: capture time, andheadline.default.actionsare 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.