Fruitful Docs
Reference

Binding package

Every field of a binding package manifest and its dom selector document.

A binding package: the package-authored configuration for one extractor engine on one site. The engine (dom, rss, http, reader, media) is platform code; the binding is data, so no package code runs during extraction. The manifest names the engine and the Lexicon def the output conforms to; the document, where the engine takes one, holds the configuration (CSS selectors for dom).

Binding documents (binding.json) use https://fruitful.app/schemas/fruitful-binding.schema.json as their $schema.

main

The binding package manifest (fruitful-package.json for kind binding).

FieldTypeRequiredDescription
kind"binding"yesPackage kind discriminator shared with lexicon and page-plugin package manifests.
namestringyesReverse-DNS package name. Immutable across versions.
versionstringyesSemantic version of this immutable release.
extractor"dom@1" | "rss@1" | "http@1" | "reader@1" | "media@1"yesThe platform extractor engine this binding configures. dom runs CSS selectors over a browser snapshot; reader hands the snapshot to the platform reader (a webDocument); media runs the platform video and audio pipeline on the URL; rss and http read a fetched body (declared; their runtimes are not available yet).
filesstring[]yesEvery file shipped in the package, as package-relative paths. Empty for bindings whose engine takes no document.
emitsstringyesLexicon def the output conforms to, as nsid@range#def (or nsid@range for a record type, or #def for a def in a Lexicon shipped by this package). The engine constrains it: reader always emits app.fruitful.content.webDocument, media emits a media record; dom, rss and http emit whatever def the document is written against. When it differs from the consuming package's transform.input, that package's transform must export normalize.
documentstringnoPackage-relative path to the binding document (see #document). Required for engines that take one (dom); absent otherwise. Must appear in files.

Example:

{
  "kind": "binding",
  "name": "com.linkedin.posts.binding.classic",
  "version": "2.1.0",
  "extractor": "dom@1",
  "files": [
    "binding.json"
  ],
  "emits": "app.fruitful.content.linkedinPost@^1.0.0#extract",
  "document": "binding.json"
}

document

The dom engine's selector set. entry finds each item; fields read values relative to it.

FieldTypeRequiredDescription
entrystringyesCSS selector matching one item per element.
fieldsfield[]yesFields read from each entry element. Field names are the extract def's property names.

Example:

{
  "entry": ".feed-shared-update-v2[data-urn^=\"urn:li:activity\"]",
  "fields": [
    {
      "name": "urn",
      "selector": ":scope",
      "attribute": "data-urn",
      "many": false
    },
    {
      "name": "url",
      "selector": "[data-post-url]",
      "attribute": "href",
      "many": false
    },
    {
      "name": "textContent",
      "selector": ".update-components-text",
      "attribute": "textContent",
      "many": false
    },
    {
      "name": "author",
      "selector": ".update-components-actor__container",
      "fields": [
        {
          "name": "name",
          "selector": ".update-components-actor__title [dir='ltr'] > span:not(.visually-hidden)",
          "attribute": "textContent",
          "many": false
        },
        {
          "name": "url",
          "selector": ".update-components-actor__meta-link",
          "attribute": "href",
          "many": false
        },
        {
          "name": "avatarUrl",
          "selector": ".update-components-actor__avatar-image",
          "attribute": "src",
          "many": false
        },
        {
          "name": "followerCount",
          "selector": ".update-components-actor__description > span:not(.visually-hidden)",
          "attribute": "textContent",
          "many": false
        },
        {
          "name": "headline",
          "selector": ".update-components-actor__description > span:not(.visually-hidden)",
          "attribute": "textContent",
          "many": false
        }
      ],
      "many": false
    },
    {
      "name": "publishedAt",
      "selector": ".update-components-actor__sub-description > span:not(.visually-hidden)",
      "attribute": "textContent",
      "many": false
    },
    {
      "name": "reactionCount",
      "selector": ".social-details-social-counts__reactions-count",
      "attribute": "textContent",
      "many": false
    },
    {
      "name": "commentCount",
      "selector": ".social-details-social-counts__comments button",
      "attribute": "textContent",
      "many": false
    },
    {
      "name": "repostHeader",
      "selector": ".update-components-header",
      "fields": [
        {
          "name": "name",
          "selector": "a",
          "attribute": "textContent",
          "many": false
        },
        {
          "name": "url",
          "selector": "a",
          "attribute": "href",
          "many": false
        },
        {
          "name": "avatarUrl",
          "selector": ".update-components-header__image img",
          "attribute": "src",
          "many": false
        }
      ],
      "many": false
    },
    {
      "name": "quotedPost",
      "selector": ".feed-shared-update-v2__update-content-wrapper.artdeco-card",
      "fields": [
        {
          "name": "authorName",
          "selector": ".update-components-actor__container .update-components-actor__title [dir='ltr'] > span:not(.visually-hidden)",
          "attribute": "textContent",
          "many": false
        },
        {
          "name": "authorUrl",
          "selector": ".update-components-actor__container .update-components-actor__meta-link",
          "attribute": "href",
          "many": false
        },
        {
          "name": "authorAvatarUrl",
          "selector": ".update-components-actor__container .update-components-actor__avatar-image",
          "attribute": "src",
          "many": false
        },
        {
          "name": "headline",
          "selector": ".update-components-actor__container .update-components-actor__description > span:not(.visually-hidden)",
          "attribute": "textContent",
          "many": false
        },
        {
          "name": "textContent",
          "selector": ".update-components-text",
          "attribute": "textContent",
          "many": false
        },
        {
          "name": "url",
          "selector": ".update-components-mini-update-v2__link-to-details-page",
          "attribute": "href",
          "many": false
        }
      ],
      "many": false
    },
    {
      "name": "images",
      "selector": ".update-components-image .update-components-image__image",
      "attribute": "src",
      "many": true
    },
    {
      "name": "video",
      "selector": ".update-components-linkedin-video",
      "fields": [
        {
          "name": "thumbnailUrl",
          "selector": "video",
          "attribute": "poster",
          "many": false
        }
      ],
      "many": false
    },
    {
      "name": "article",
      "selector": "article.update-components-article:not(.update-components-article-first-party)",
      "fields": [
        {
          "name": "title",
          "selector": ".update-components-article__title",
          "attribute": "textContent",
          "many": false
        },
        {
          "name": "source",
          "selector": ".update-components-article__subtitle--inset",
          "attribute": "textContent",
          "many": false
        },
        {
          "name": "url",
          "selector": "a.tap-target",
          "attribute": "href",
          "many": false
        },
        {
          "name": "imageUrl",
          "selector": "img",
          "attribute": "src",
          "many": false
        }
      ],
      "many": false
    },
    {
      "name": "newsletter",
      "selector": "article.update-components-article-first-party",
      "fields": [
        {
          "name": "title",
          "selector": ".update-components-article-first-party__title",
          "attribute": "textContent",
          "many": false
        },
        {
          "name": "authorName",
          "selector": ".update-components-article-first-party__subtitle",
          "attribute": "textContent",
          "many": false
        },
        {
          "name": "newsletterName",
          "selector": ".update-components-article-newsletter-title__text span[aria-hidden='true']",
          "attribute": "textContent",
          "many": false
        },
        {
          "name": "url",
          "selector": "a.tap-target",
          "attribute": "href",
          "many": false
        },
        {
          "name": "imageUrl",
          "selector": ".update-components-article-first-party__image",
          "attribute": "src",
          "many": false
        }
      ],
      "many": false
    },
    {
      "name": "document",
      "selector": ".update-components-document__container",
      "fields": [
        {
          "name": "title",
          "selector": "iframe",
          "attribute": "title",
          "many": false
        }
      ],
      "many": false
    },
    {
      "name": "poll",
      "selector": ".update-components-poll",
      "fields": [
        {
          "name": "question",
          "selector": ".update-components-poll__header h3",
          "attribute": "textContent",
          "many": false
        },
        {
          "name": "totalVotes",
          "selector": ".update-components-poll-summary__subtext-container > button:first-of-type",
          "attribute": "textContent",
          "many": false
        },
        {
          "name": "timeRemaining",
          "selector": ".update-components-poll-summary__option-text span[aria-hidden='true']",
          "attribute": "textContent",
          "many": false
        }
      ],
      "many": false
    },
    {
      "name": "pollOptions",
      "selector": ".update-components-poll-option",
      "fields": [
        {
          "name": "text",
          "selector": ".update-components-poll-option__text-container span.t-14.t-bold",
          "attribute": "textContent",
          "many": false
        },
        {
          "name": "percentage",
          "selector": ".update-components-poll-option__percentage",
          "attribute": "textContent",
          "many": false
        }
      ],
      "many": true
    },
    {
      "name": "job",
      "selector": ".update-components-entity",
      "fields": [
        {
          "name": "title",
          "selector": ".update-components-entity__title",
          "attribute": "textContent",
          "many": false
        },
        {
          "name": "companyName",
          "selector": ".update-components-entity__subtitle",
          "attribute": "textContent",
          "many": false
        },
        {
          "name": "location",
          "selector": ".update-components-entity__description",
          "attribute": "textContent",
          "many": false
        },
        {
          "name": "url",
          "selector": ".update-components-entity__content",
          "attribute": "href",
          "many": false
        },
        {
          "name": "companyLogoUrl",
          "selector": ".update-components-entity__image-container img",
          "attribute": "src",
          "many": false
        }
      ],
      "many": false
    }
  ]
}

field

One extract field. With attribute it reads a string (or a boolean for exists); with nested fields it reads an object. many collects every match as an array.

FieldTypeRequiredDescription
namestringyesProperty name in the extract def.
selectorstringyesCSS selector relative to the parent element. :scope selects the parent itself.
attributestringnoDOM property or HTML attribute to read: textContent, innerHTML, exists, or an attribute name such as href or data-urn. Omit when fields is present.
manybooleannoCollect every match as an array instead of the first match.
fieldsfield[]noNested fields read relative to each matched element, producing an object. Omit when attribute is present.

Example:

{
  "name": "urn",
  "selector": ":scope",
  "attribute": "data-urn",
  "many": false
}

On this page