Fruitful Docs
Get started

Your first package

Follow a working package from its product result to its files and validation commands.

This tutorial connects a working Hacker News package to its result in Fruitful. You can examine the preview before you install tools or read package files.

See it in the product

This Product Preview uses the product navigation, Activity rows, Reader, and Fruitful theme. It shows a selected Public Example from February 23, 2026. The preview loads without an account. Its desktop layout scales to fit this page. Use the corner control to switch between actual size and all three panes.

Loading workspace…

Open DevTools along the bottom to inspect components, data, and captured fields, or experiment in the official A2UI Composer. Expand the workspace to use the editor at full size.

Select Six Math Essentials or Worg to read the two included articles. At actual size, scroll inside the preview to reach the other panes. Previous and next controls work locally. Website and discussion links open their public destinations. The other stories state that their articles are not included. Following, account settings, capture, and saving are inactive in this example.

The package files below define the capture, records, and presentation used by this example.

The definition

feed-packages/hacker-news/
├── fruitful-feed-package.json      # the definition: which packages, where review evidence is
├── fruitful-package.json           # THE manifest: capture, transform, render, focus
├── fruitful-lexicons.json          # where generated authoring types go
├── fruitful-plugin-build.json      # bundle entrypoint, outfile, released bundle digests
├── lexicons/                       # this package's record Lexicons and its View Lexicon
├── bindings/front-page/            # a dom binding: selectors that produce the extract
├── authoring/src/                  # the transform, in TypeScript
├── plugin/                         # the shipped payload: bundled transform and surfaces
└── review/                         # fixtures, expectations, presentation examples, evidence

The file explorer reads the package from the repository at build time. Select a file to examine its contents.

The explorer omits the generated bundle, the large capture fixture, and its generated expectation. Their paths are plugin/hn-page-plugin.js, review/fixtures/front-page.html, and review/expectations/front-page.json. It also omits generated Lexicon types under authoring/src/generated/. The Lexicon generator writes those types from lexicons/.

fruitful-feed-package.json lists the packages the definition produces, in dependency order:

{
  "schemaVersion": 2,
  "packages": [
    "lexicons/submission", "lexicons/account", "lexicons/defs", "bindings/front-page",
    { "root": "plugin", "manifest": "fruitful-package.json" }
  ],
  "reviewEvidenceManifest": "review/review-evidence.json"
}

The definition contains three Lexicon packages, one binding package, and the Feed Package. Each registry package has an immutable version and its own manifest. Lexicon and binding manifests declare their names, versions, and files inside their directories. The Feed Package uses the root fruitful-package.json, as the object entry shows. Its files paths are relative to plugin/, which contains the exact runtime payload. Typed source stays under authoring/, outside that payload, so source edits alone do not change released bytes.

The Lexicon: what a record is

lexicons/submission/lexicon.json defines app.fruitful.feed.hackerNews.submission. It has two defs that matter here:

  • main is the record: a stable submission identity (uri, itemId, title) plus the observed data (submitter, submittedAt, destinationUrl, site, points, commentCount).
  • extract is what the binding produces before the transform runs: itemId, title, rank, destinationUrl, site, submitterUsername, submittedAt, points, commentCount.

The extract represents the page data. The record represents the product data. The transform converts the extract into records.

The binding: how the page becomes an extract

A binding contains data for the extraction engine. bindings/front-page/fruitful-package.json names the engine and its output contract:

{
  "kind": "binding",
  "name": "com.ycombinator.news.binding.front-page",
  "version": "0.3.0",
  "extractor": "dom@1",
  "files": ["binding.json"],
  "emits": "app.fruitful.feed.hackerNews.submission@^1.0.0#extract",
  "document": "binding.json"
}

binding.json contains selectors that dom@1 applies to the captured page. entry matches one submission. The engine reads each field relative to that entry:

{
  "entry": "tr.submission",
  "fields": [
    { "name": "itemId", "selector": ":scope", "attribute": "id" },
    { "name": "title", "selector": "td.title span.titleline > a", "attribute": "textContent" },
    { "name": "destinationUrl", "selector": "td.title span.titleline > a", "attribute": "href" },
    { "name": "submittedAt", "selector": ":scope + tr span.age", "attribute": "title" }
  ]
}

Field names match the extract def's property names. Fruitful validates engine output against the Lexicon before the transform runs.

The manifest: wiring it together

The root fruitful-package.json is the Feed Package manifest. Its four blocks follow the data-flow order. Scroll or click through the steps to examine excerpts from the file.

the header

The header declares the package name, immutable version, runtime contract, and files. The files list declares each shipped file relative to plugin/. Undeclared files cannot enter the runtime artifact.

capture

The capture block declares two routes and two binding pins. The submissions collection route has a feed schedule that requests a capture each two hours. The article item route captures each submission's destinationUrl through the first-party reader binding. That route supplies the article for a headline.

transform

The transform block declares the bundled module and the record types it can emit. Other record types cannot leave the sandbox. The transform section explains its TypeScript source.

render

The render block declares the root record, its View, two A2UI surfaces, and explicit actions. A package offers external content through a declared action. Here, render.actions lets Activity open the article in Reader.

focus

Focus changes the live website through the extension when you follow a listed feed. It operates separately from capture and never runs package code. A policy can replace an element with a Fruitful panel through replace. It can also hide elements through adblock-syntax cosmetic rules in hide. Hacker News uses no hide rules. X uses nine rules for its sidebar and trends, while LinkedIn uses eight for its right column.

{
"schema": 3,
"kind": "page-plugin",
"name": "com.ycombinator.news",
"version": "0.17.0",
"runtime": "fruitful-page-plugin@1",
"files": ["hn-page-plugin.js", "presentation/compact.surface.json", "presentation/reader.surface.json"]
}

For sites that need blocking, put hide beside replace in the same policy. This example shows rules from X:

feed-packages/x — home-focus
"focus": [
  {
    "hide": [
      "x.com##[data-testid=\"sidebarColumn\"]",
      "twitter.com##[data-testid=\"sidebarColumn\"]",
      "www.twitter.com##[data-testid=\"sidebarColumn\"]"
    ],
    "replace": { "selector": "[aria-label^=\"Timeline\"]", "with": "panel", "title": "Your Fruitful briefing is ready here." }
  }
]

Read Focus policies for the rules. Focus is not part of the pipeline explains their separate operation. Use Release a version when the package is ready. The manifest reference describes each field.

The transform: extract to records

authoring/src/hn-page-plugin.ts exports two functions. materializeRecords converts extract items into Lexicon records through generated builders:

materializeRecords(items) {
  const records = [];
  for (const item of items) {
    const itemId = string(item.fields['itemId']);
    const title = string(item.fields['title']);
    if (!itemId || !title) continue;
    records.push(Submission.$build({ uri: `https://news.ycombinator.com/item?id=${itemId}`, itemId, title, /* ... */ }));
  }
  return records;
}

buildView converts the root and related records into the package's View. The surfaces read that View. The generator bundles the module into plugin/hn-page-plugin.js. It runs in a sandbox without host, network, or filesystem access. Read Plugin runtime environment for available operations.

The generated builders come from the Lexicons. Make sure they are current:

yarn fruitful lexicon generate feed-packages/hacker-news --check

The surfaces: how it renders

plugin/presentation/compact.surface.json and reader.surface.json contain A2UI v0.9.1 templates. These templates bind to the View through JSON Pointer. The compact surface contains a title and a metadata row:

{ "id": "title", "component": "Text", "text": { "path": "/record/title" }, "variant": "h5" }

The package never ships client code. Fruitful renders these components from its trusted catalog.

The package's compact surface appears in each Activity row. Its expanded surface supplies the original update in Source details. Use the Reader toolbar's information button to open Source details. Reader shows the linked article from a resource declared by the package.

Use the component catalog to examine individual surfaces and resolved data.

Review evidence: proving it works

review/review-evidence.json declares cases. Each case pairs a sanitized capture fixture with expected records and a presentation example:

{
  "id": "public-front-page-capture",
  "displayName": "Front Page",
  "format": "html",
  "fixture": "review/fixtures/front-page.html",
  "url": "https://news.ycombinator.com/",
  "binding": "front-page",
  "expectation": "review/expectations/front-page.json",
  "provenance": { "kind": "sanitized-capture", "evidence": "review/evidence/front-page.json" }
}

Review evidence never ships to users. It is what validation runs against.

Run the checks

Install the CLI before you run these commands. These three commands run offline against the stored review cases.

Coverage shows what the binding extracts from each fixture:

yarn fruitful plugin coverage feed-packages/hacker-news --json
{
  "success": true,
  "fixtures": [{
    "fixture": "review/fixtures/front-page.html",
    "coverage": {
      "entrySelector": "tr.submission",
      "matchedEntryCount": 30,
      "extractedEntryCount": 30,
      "requiredFields": ["itemId", "rank", "title"],
      "neverObservedFields": [],
      "fields": [{ "path": "title", "presentCount": 30, "totalCount": 30 }]
    }
  }]
}

Validate runs the full package over each case. Its checks cover execution, determinism, coverage, Lexicons, Views, goldens, and presentation:

yarn fruitful plugin validate feed-packages/hacker-news --json
{
  "success": true,
  "root": { "name": "com.ycombinator.news", "version": "0.17.0" },
  "resolutionDigest": "sha256:…",
  "checks": {
    "execution": "passed", "determinism": "passed", "coverage": "passed",
    "lexicons": "passed", "views": "passed", "goldens": "passed", "presentation": "passed"
  },
  "cases": [{ "caseId": "public-front-page-capture", "recordCount": 59, "goldenAgreement": "matched" }]
}

Generate rebuilds the bundle, expectations, and presentation examples. --check fails if anything committed is stale:

yarn fruitful plugin generate feed-packages/hacker-news --check --json

Preview your changes

Use Fruitful DevTools to inspect UI, data, and captured fields, or create a disposable Composer draft.

The embedded example above uses a stored Public Example. It does not change when you edit package files. After edits, follow the validation procedure. Then create a preview from your local package and its stored evidence:

yarn fruitful plugin preview feed-packages/hacker-news --json

Use Preview in the product to examine the result and repair incorrect entries.

For the embedded example, the export process materializes the package in a separate disposable local workspace. It reads the selected content through the product's Activity APIs. Regeneration tests the selected records, Views, and resolved articles. Repository contributors use yarn preview:export --check to find stale output. Each article shows its public provenance and attribution after the text. The preview runs in your browser without a connection to the product API.

Now for your own site

Start with the page URL and one entry you expect Fruitful to collect. You can use an agent with the feed-package-authoring skill or follow the task guides directly. For an existing site, edit its package. For an incorrect or missing entry, use the correction procedure.

For a new site, select a working package with almost the same capture and record requirements. Hacker News provides a public collection example. feed-packages/linkedin provides an authenticated example with different entry shapes. Use Choose evidence and Prepare the capture route for the current setup procedure.

Choose the guide for the part you must change:

TaskGuide
Select fields from a pageBinding packages
Define records and linksLexicons
Convert extracts to recordsNormalize and transform
Control captureCapture hooks, scrolling and pagination
Prepare the shown dataPresentation Views
Select the shown componentsCompact and reader surfaces
Change the live website through the extensionFocus policies

The manifest reference defines the package fields. The relationship rules define constraints between those fields. Read the runtime environment before you write package code.

Follow the validation procedure after edits. Then examine the package preview. Use Release a version to prepare a release. Report the result with that guide's completion states.

Next: Publish it to the registry.

On this page