@ecency/render-helper 2.4.14 → 2.4.15

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.
@@ -9,7 +9,20 @@ interface Entry {
9
9
  json_metadata?: any;
10
10
  }
11
11
 
12
- declare function markdown2Html(obj: Entry | string, forApp?: boolean, webp?: boolean, parentDomain?: string): string;
12
+ /**
13
+ * SEO context for controlling rel attributes on external links in user-generated content.
14
+ *
15
+ * By default, all external links get rel="nofollow ugc noopener" to prevent link spam.
16
+ * High-quality content (high author reputation + meaningful post rewards) earns followed links.
17
+ */
18
+ interface SeoContext {
19
+ /** Human-readable author reputation score (after accountReputation() conversion) */
20
+ authorReputation?: number;
21
+ /** Total post payout in USD */
22
+ postPayout?: number;
23
+ }
24
+
25
+ declare function markdown2Html(obj: Entry | string, forApp?: boolean, webp?: boolean, parentDomain?: string, seoContext?: SeoContext): string;
13
26
 
14
27
  declare function catchPostImage(obj: Entry | string, width?: number, height?: number, format?: string): string | null;
15
28
 
@@ -32,4 +45,4 @@ declare const SECTION_LIST: string[];
32
45
 
33
46
  declare function isValidPermlink(permlink: string): boolean;
34
47
 
35
- export { type Entry, SECTION_LIST, catchPostImage, isValidPermlink, getPostBodySummary as postBodySummary, proxifyImageSrc, markdown2Html as renderPostBody, setCacheSize, setProxyBase };
48
+ export { type Entry, SECTION_LIST, type SeoContext, catchPostImage, isValidPermlink, getPostBodySummary as postBodySummary, proxifyImageSrc, markdown2Html as renderPostBody, setCacheSize, setProxyBase };
@@ -388,6 +388,14 @@ function createImageHTML(src, isLCP, webp) {
388
388
  }
389
389
 
390
390
  // src/methods/a.method.ts
391
+ var NOFOLLOW_REPUTATION_THRESHOLD = 40;
392
+ var FOLLOW_PAYOUT_THRESHOLD = 5;
393
+ function getExternalLinkRel(seoContext) {
394
+ if (seoContext?.authorReputation !== void 0 && seoContext?.postPayout !== void 0 && seoContext.authorReputation >= NOFOLLOW_REPUTATION_THRESHOLD && seoContext.postPayout > FOLLOW_PAYOUT_THRESHOLD) {
395
+ return "noopener";
396
+ }
397
+ return "nofollow ugc noopener";
398
+ }
391
399
  var normalizeValue = (value) => value ? value.trim() : "";
392
400
  var matchesHref = (href, value) => {
393
401
  const normalizedHref = normalizeValue(href);
@@ -421,7 +429,7 @@ var addLineBreakBeforePostLink = (el, forApp, isInline) => {
421
429
  el.parentNode.insertBefore(br, el);
422
430
  }
423
431
  };
424
- function a(el, forApp, webp, parentDomain = "ecency.com") {
432
+ function a(el, forApp, webp, parentDomain = "ecency.com", seoContext) {
425
433
  if (!el || !el.parentNode) {
426
434
  return;
427
435
  }
@@ -981,7 +989,7 @@ function a(el, forApp, webp, parentDomain = "ecency.com") {
981
989
  el.setAttribute("class", "markdown-internal-link");
982
990
  } else {
983
991
  el.setAttribute("target", "_blank");
984
- el.setAttribute("rel", "noopener");
992
+ el.setAttribute("rel", getExternalLinkRel(seoContext));
985
993
  }
986
994
  el.setAttribute("href", href);
987
995
  }
@@ -1270,7 +1278,7 @@ function text(node, forApp, webp) {
1270
1278
  }
1271
1279
 
1272
1280
  // src/methods/traverse.method.ts
1273
- function traverse(node, forApp, depth = 0, webp = false, state = { firstImageFound: false }, parentDomain = "ecency.com") {
1281
+ function traverse(node, forApp, depth = 0, webp = false, state = { firstImageFound: false }, parentDomain = "ecency.com", seoContext) {
1274
1282
  if (!node || !node.childNodes) {
1275
1283
  return;
1276
1284
  }
@@ -1278,7 +1286,7 @@ function traverse(node, forApp, depth = 0, webp = false, state = { firstImageFou
1278
1286
  const child = node.childNodes[i];
1279
1287
  if (!child) return;
1280
1288
  if (child.nodeName.toLowerCase() === "a") {
1281
- a(child, forApp, webp, parentDomain);
1289
+ a(child, forApp, webp, parentDomain, seoContext);
1282
1290
  }
1283
1291
  if (child.nodeName.toLowerCase() === "iframe") {
1284
1292
  iframe(child, parentDomain);
@@ -1294,7 +1302,7 @@ function traverse(node, forApp, depth = 0, webp = false, state = { firstImageFou
1294
1302
  }
1295
1303
  const currentChild = node.childNodes[i];
1296
1304
  if (currentChild) {
1297
- traverse(currentChild, forApp, depth + 1, webp, state, parentDomain);
1305
+ traverse(currentChild, forApp, depth + 1, webp, state, parentDomain, seoContext);
1298
1306
  }
1299
1307
  });
1300
1308
  }
@@ -1345,7 +1353,7 @@ function fixBlockLevelTagsInParagraphs(html) {
1345
1353
  html = html.replace(/<p><br>\s*<\/p>/g, "");
1346
1354
  return html;
1347
1355
  }
1348
- function markdownToHTML(input, forApp, webp, parentDomain = "ecency.com") {
1356
+ function markdownToHTML(input, forApp, webp, parentDomain = "ecency.com", seoContext) {
1349
1357
  input = input.replace(new RegExp("https://leofinance.io/threads/view/", "g"), "/@");
1350
1358
  input = input.replace(new RegExp("https://leofinance.io/posts/", "g"), "/@");
1351
1359
  input = input.replace(new RegExp("https://leofinance.io/threads/", "g"), "/@");
@@ -1405,7 +1413,7 @@ function markdownToHTML(input, forApp, webp, parentDomain = "ecency.com") {
1405
1413
  output = md.render(input);
1406
1414
  output = fixBlockLevelTagsInParagraphs(output);
1407
1415
  const doc = DOMParser.parseFromString(`<body id="root">${removeDuplicateAttributes(output)}</body>`, "text/html");
1408
- traverse(doc, forApp, 0, webp, { firstImageFound: false }, parentDomain);
1416
+ traverse(doc, forApp, 0, webp, { firstImageFound: false }, parentDomain, seoContext);
1409
1417
  output = serializer.serializeToString(doc);
1410
1418
  } catch (error) {
1411
1419
  try {
@@ -1418,7 +1426,7 @@ function markdownToHTML(input, forApp, webp, parentDomain = "ecency.com") {
1418
1426
  });
1419
1427
  const repairedHtml = domSerializer(dom.children);
1420
1428
  const doc = DOMParser.parseFromString(`<body id="root">${removeDuplicateAttributes(repairedHtml)}</body>`, "text/html");
1421
- traverse(doc, forApp, 0, webp, { firstImageFound: false }, parentDomain);
1429
+ traverse(doc, forApp, 0, webp, { firstImageFound: false }, parentDomain, seoContext);
1422
1430
  output = serializer.serializeToString(doc);
1423
1431
  } catch (fallbackError) {
1424
1432
  const escapedContent = he2.encode(output || md.render(input));
@@ -1446,18 +1454,18 @@ function cacheSet(key, value) {
1446
1454
  }
1447
1455
 
1448
1456
  // src/markdown-2-html.ts
1449
- function markdown2Html(obj, forApp = true, webp = false, parentDomain = "ecency.com") {
1457
+ function markdown2Html(obj, forApp = true, webp = false, parentDomain = "ecency.com", seoContext) {
1450
1458
  if (typeof obj === "string") {
1451
1459
  const cleanedStr = cleanReply(obj);
1452
- return markdownToHTML(cleanedStr, forApp, webp, parentDomain);
1460
+ return markdownToHTML(cleanedStr, forApp, webp, parentDomain, seoContext);
1453
1461
  }
1454
- const key = `${makeEntryCacheKey(obj)}-md${webp ? "-webp" : ""}-${forApp ? "app" : "site"}-${parentDomain}`;
1462
+ const key = `${makeEntryCacheKey(obj)}-md${webp ? "-webp" : ""}-${forApp ? "app" : "site"}-${parentDomain}${seoContext ? `-seo${seoContext.authorReputation ?? ""}-${seoContext.postPayout ?? ""}` : ""}`;
1455
1463
  const item = cacheGet(key);
1456
1464
  if (item) {
1457
1465
  return item;
1458
1466
  }
1459
1467
  const cleanBody = cleanReply(obj.body);
1460
- const res = markdownToHTML(cleanBody, forApp, webp, parentDomain);
1468
+ const res = markdownToHTML(cleanBody, forApp, webp, parentDomain, seoContext);
1461
1469
  cacheSet(key, res);
1462
1470
  return res;
1463
1471
  }