@sockethub/platform-metadata 1.0.1-beta.4 → 1.0.1-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -98,6 +98,11 @@ The platform extracts the following metadata when available:
98
98
  flattened to one string, or the virtual location URL), `url`, and `status`
99
99
  (`scheduled`, `cancelled`, `postponed`, `rescheduled`). Only present when
100
100
  both a name and a start date are declared.
101
+ * **Description fallback**: pages with no meta description use a JSON-LD
102
+ `description`, else a `headline` not already in the title (Wikipedia's
103
+ short description, e.g. "1979 studio album by Brian Eno").
104
+ * **Non-HTML links** (PDFs and other files) return a page titled with the
105
+ file name instead of an error.
101
106
 
102
107
  ## Site-Specific Handling
103
108
 
@@ -108,7 +113,9 @@ so those URLs are resolved through preview services built for this purpose:
108
113
  are fetched from the [FxTwitter API](https://github.com/FxEmbed/FxEmbed)
109
114
  instead of scraped — X serves only a generic site banner to scrapers. The
110
115
  response carries the post's text, its actual photo (or video + poster
111
- thumbnail), and the canonical `x.com` URL. If the API is unavailable or the
116
+ thumbnail), and the canonical `x.com` URL. Posts without their own media
117
+ use the quoted post's media, then the link card image, then the author's
118
+ avatar. If the API is unavailable or the
112
119
  post can't be resolved, the platform falls back to the regular scrape
113
120
  (which still yields the post text).
114
121
  * **Reddit post URLs** use `old.reddit.com`'s structured JSON response, avoiding
@@ -127,7 +134,10 @@ so those URLs are resolved through preview services built for this purpose:
127
134
  * **Amazon product URLs** (storefronts such as `amazon.de` and `amazon.com`,
128
135
  plus `amzn.to`/`a.co` short links) are scraped with WhatsApp's link-preview
129
136
  user agent. Amazon answers other crawlers with a captcha or a generic
130
- "Amazon" share card instead of the product's title and image.
137
+ "Amazon" share card instead of the product's title and image. Mobile-app
138
+ links (`/gp/aw/d/<ASIN>`) are scraped as `/dp/<ASIN>`, which is the only
139
+ form that gets the product card. When Amazon serves its full desktop page
140
+ instead, the image comes from the page's main product image.
131
141
  * **Facebook engagement prefixes**: video titles and image alt text often
132
142
  start with a localized view or reaction count, such as
133
143
  `2.2M views · 21K reactions | …` or `347K views | …`. When the prefix is
package/dist/index.js CHANGED
@@ -70777,7 +70777,7 @@ var import_undici3 = __toESM(require_undici(), 1);
70777
70777
  // package.json
70778
70778
  var package_default = {
70779
70779
  name: "@sockethub/platform-metadata",
70780
- version: "1.0.1-beta.4",
70780
+ version: "1.0.1-beta.6",
70781
70781
  description: "Fetch metadata from a given URL",
70782
70782
  author: "Nick Jennings <nick@silverbucket.net>",
70783
70783
  license: "LGPL-3.0+",
@@ -70819,17 +70819,17 @@ var package_default = {
70819
70819
  "clean:deps": "rm -rf node_modules"
70820
70820
  },
70821
70821
  dependencies: {
70822
- "@sockethub/util": "^1.0.0-beta.4",
70822
+ "@sockethub/util": "^1.0.0-beta.6",
70823
70823
  "open-graph-scraper": "^6.11.0",
70824
70824
  undici: "^7.30.0"
70825
70825
  },
70826
70826
  devDependencies: {
70827
- "@sockethub/schemas": "^3.0.0-beta.4"
70827
+ "@sockethub/schemas": "^3.0.0-beta.6"
70828
70828
  },
70829
70829
  peerDependencies: {
70830
- "@sockethub/server": "^5.0.0-beta.4"
70830
+ "@sockethub/server": "^5.0.0-beta.6"
70831
70831
  },
70832
- gitHead: "639c296a7332f192a1b3f7e86ef6603b5b67bbaa",
70832
+ gitHead: "604f3da8850a19a4fd5f5e5dcec10bf283e9085e",
70833
70833
  types: "./dist/index.d.ts"
70834
70834
  };
70835
70835
 
@@ -71112,6 +71112,30 @@ function isAmazonUrl(url) {
71112
71112
  return true;
71113
71113
  return host.startsWith("amazon.") && AMAZON_STORE_TLDS.has(host.slice(7));
71114
71114
  }
71115
+ function resolveAmazonAppLink(url) {
71116
+ if (!isAmazonUrl(url))
71117
+ return null;
71118
+ const parsed = new URL(url);
71119
+ const asin = parsed.pathname.match(/^\/gp\/aw\/d\/([A-Z0-9]{10})(?:\/|$)/i)?.[1];
71120
+ return asin ? `${parsed.origin}/dp/${asin}` : null;
71121
+ }
71122
+ function extractAmazonImage(html) {
71123
+ const tag = html?.match(/<img[^>]*\bid="landingImage"[^>]*>/)?.[0];
71124
+ return tag?.match(/\bdata-old-hires="(https:[^"]+)"/)?.[1];
71125
+ }
71126
+ function schemaOrgSummary(jsonLD, title = "") {
71127
+ if (!Array.isArray(jsonLD))
71128
+ return;
71129
+ const items = jsonLD.flatMap((item) => item?.["@graph"] ?? item);
71130
+ const description = items.find((item) => typeof item?.description === "string" && item.description.trim())?.description;
71131
+ if (description)
71132
+ return description;
71133
+ for (const item of items) {
71134
+ const headline = typeof item?.headline === "string" ? item.headline.trim() : "";
71135
+ if (headline && !title.includes(headline))
71136
+ return headline;
71137
+ }
71138
+ }
71115
71139
  function stripFacebookEngagement(title) {
71116
71140
  const bar = title?.indexOf("|") ?? -1;
71117
71141
  if (title === undefined || bar === -1) {
@@ -71338,10 +71362,12 @@ function tweetToPageObject(status) {
71338
71362
 
71339
71363
  `) : undefined;
71340
71364
  const title = typeof article?.title === "string" && article.title || (author.name && author.screen_name ? `${author.name} (@${author.screen_name}) on X` : author.name ?? "Post on X");
71341
- const photo = tweet.media?.photos?.[0];
71342
- const video = tweet.media?.videos?.[0];
71365
+ const media = tweet.media?.photos?.length || tweet.media?.videos?.length ? tweet.media : tweet.quote?.media;
71366
+ const photo = media?.photos?.[0];
71367
+ const video = media?.videos?.[0];
71343
71368
  const articleImage = article?.cover_media?.media_info;
71344
- const imageUrl = articleImage?.original_img_url ?? photo?.url ?? video?.thumbnail_url;
71369
+ const card = tweet.card?.image;
71370
+ const imageUrl = articleImage?.original_img_url ?? photo?.url ?? video?.thumbnail_url ?? card?.url ?? author.avatar_url;
71345
71371
  const page = {
71346
71372
  type: "page",
71347
71373
  title,
@@ -71354,8 +71380,8 @@ function tweetToPageObject(status) {
71354
71380
  page.image = [
71355
71381
  {
71356
71382
  url: imageUrl,
71357
- width: articleImage?.original_img_width ?? photo?.width ?? video?.width,
71358
- height: articleImage?.original_img_height ?? photo?.height ?? video?.height
71383
+ width: articleImage?.original_img_width ?? photo?.width ?? video?.width ?? card?.width,
71384
+ height: articleImage?.original_img_height ?? photo?.height ?? video?.height ?? card?.height
71359
71385
  }
71360
71386
  ];
71361
71387
  }
@@ -71610,7 +71636,8 @@ class Metadata {
71610
71636
  this.fetchDirectImage(job, image, cb);
71611
71637
  return;
71612
71638
  }
71613
- this.scrape(job, cb);
71639
+ const amazonUrl = resolveAmazonAppLink(job.actor.id);
71640
+ this.scrape(job, cb, undefined, amazonUrl ?? job.actor.id);
71614
71641
  }
71615
71642
  async fetchDirectImage(job, candidate, cb, timeoutMs = DIRECT_IMAGE_PROBE_TIMEOUT_MS) {
71616
71643
  try {
@@ -71935,6 +71962,10 @@ class Metadata {
71935
71962
  }
71936
71963
  }
71937
71964
  }
71965
+ const amazonImage = amazon && !result.ogImage?.[0]?.url?.includes("/images/I/") && extractAmazonImage(data.html);
71966
+ if (amazonImage) {
71967
+ result = { ...result, ogImage: [{ url: amazonImage }] };
71968
+ }
71938
71969
  const embed = reddit ? await redditEmbed : undefined;
71939
71970
  const youtube = await youtubeEmbed;
71940
71971
  if (!reddit)
@@ -71950,7 +71981,7 @@ class Metadata {
71950
71981
  language: result.ogLocale,
71951
71982
  title: embed?.title ?? youtube?.title ?? (facebook ? stripFacebookEngagement(result.ogTitle) : result.ogTitle),
71952
71983
  name: embed?.provider_name ?? youtube?.provider_name ?? siteName,
71953
- description: normalizeDescription(youtubeDescription || result.ogDescription || ""),
71984
+ description: normalizeDescription(youtubeDescription || result.ogDescription || schemaOrgSummary(result.jsonLD, result.ogTitle) || ""),
71954
71985
  image: reddit ? redditPostImages(result.ogImage) ?? redditOEmbedImage(embed ?? {}) : scrapedImages?.length ? scrapedImages : youtubeOEmbedImage(youtube),
71955
71986
  url: job.actor.id,
71956
71987
  favicon: result.favicon || "/favicon.ico",
@@ -71995,6 +72026,25 @@ class Metadata {
71995
72026
  cb(null, job);
71996
72027
  return;
71997
72028
  }
72029
+ let filename = "";
72030
+ if (/must scrape an html page/i.test(err.message)) {
72031
+ try {
72032
+ filename = decodeURIComponent(new URL(job.actor.id).pathname.split("/").at(-1) || "");
72033
+ } catch {
72034
+ filename = job.actor.id.split(/[?#]/, 1)[0].split("/").at(-1) || "";
72035
+ }
72036
+ }
72037
+ if (filename) {
72038
+ job.object = {
72039
+ type: "page",
72040
+ title: filename,
72041
+ description: "",
72042
+ url: job.actor.id,
72043
+ favicon: "/favicon.ico"
72044
+ };
72045
+ cb(null, job);
72046
+ return;
72047
+ }
71998
72048
  cb(markExpectedError(new Error(`metadata scrape failed for ${job.actor.id}: ${err.message}`)));
71999
72049
  });
72000
72050
  }
@@ -72011,4 +72061,4 @@ export {
72011
72061
  withDeadline
72012
72062
  };
72013
72063
 
72014
- //# debugId=391BA00E724CA7D764756E2164756E21
72064
+ //# debugId=BFF98FE9E1E6A3E464756E2164756E21