Fruitful Docs
GuidesTransform

Write a binding package

Publish the selector document that turns a page into your extract.

A binding package configures one engine for one site. For dom@1 it is a manifest plus a selector document. It is data only: no package code runs during extraction.

The manifest

bindings/<name>/fruitful-package.json:

{
  "kind": "binding",
  "name": "com.example.blog.binding.archive",
  "version": "1.0.0",
  "extractor": "dom@1",
  "files": ["binding.json"],
  "emits": "com.example.blog.post@^1.0.0#extract",
  "document": "binding.json"
}

emits names the def the output conforms to. When it is the same def the consuming package's transform expects, nothing else is needed. When it differs, that package's transform exports normalize; see Write the normalize transform.

The document

binding.json is an entry selector and the fields read relative to each match:

{
  "$schema": "https://fruitful.app/schemas/fruitful-binding.schema.json",
  "entry": "article.post",
  "fields": [
    { "name": "title", "selector": "h2 a", "attribute": "textContent" },
    { "name": "href", "selector": "h2 a", "attribute": "href" },
    { "name": "date", "selector": "time", "attribute": "datetime" },
    { "name": "author", "selector": ".byline a", "attribute": "textContent" },
    { "name": "tags", "selector": ".tags a", "attribute": "textContent", "many": true },
    {
      "name": "image",
      "selector": "figure",
      "fields": [
        { "name": "src", "selector": "img", "attribute": "src" },
        { "name": "alt", "selector": "img", "attribute": "alt" }
      ]
    }
  ]
}
  • attribute reads textContent, innerHTML, exists (a boolean), or any attribute such as href or data-id.
  • many collects every match as an array.
  • Nested fields read an object relative to the matched element. Use attribute or fields, not both.
  • :scope selects the entry element itself. :scope + tr reaches a sibling row, which Hacker News needs because each submission spans two table rows.

Every field must be declared by the emitted extract def, and the def's required list must be covered. Validation reports both.

Check it against a fixture

yarn fruitful plugin coverage feed-packages/<name> --json

Coverage reports, per fixture, how many entries the entry selector matched, how many extracted, which required fields were missing on which entries, and any field never observed. Iterate until required fields are present on every entry and nothing is never observed, then explain any remaining gap in the package notes.

Reference: Binding package.

On this page