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).
| Field | Type | Required | Description |
|---|---|---|---|
kind | "binding" | yes | Package kind discriminator shared with lexicon and page-plugin package manifests. |
name | string | yes | Reverse-DNS package name. Immutable across versions. |
version | string | yes | Semantic version of this immutable release. |
extractor | "dom@1" | "rss@1" | "http@1" | "reader@1" | "media@1" | yes | The 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). |
files | string[] | yes | Every file shipped in the package, as package-relative paths. Empty for bindings whose engine takes no document. |
emits | string | yes | Lexicon 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. |
document | string | no | Package-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.
| Field | Type | Required | Description |
|---|---|---|---|
entry | string | yes | CSS selector matching one item per element. |
fields | field[] | yes | Fields 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.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Property name in the extract def. |
selector | string | yes | CSS selector relative to the parent element. :scope selects the parent itself. |
attribute | string | no | DOM property or HTML attribute to read: textContent, innerHTML, exists, or an attribute name such as href or data-urn. Omit when fields is present. |
many | boolean | no | Collect every match as an array instead of the first match. |
fields | field[] | no | Nested 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
}