Skip to content

Commit c20cad0

Browse files
authored
Merge pull request #170 from Alpha4615/feature/onResponse-override
Resolves #159 - Creates an onResponse c…
2 parents 07745ed + 63da12b commit c20cad0

File tree

3 files changed

+77
-2
lines changed

3 files changed

+77
-2
lines changed

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ Additionally, you can pass an options object which should add more functionality
9595
| followRedirects (**optional**) (default 'error') | For security reasons, the library does not automatically follow redirects ('error' value), a malicious agent can exploit redirects to steal data, posible values: ('error', 'follow', 'manual') |
9696
| handleRedirects (**optional**) (with followRedirects 'manual') | When followRedirects is set to 'manual' you need to pass a function that validates if the redirectinon is secure, below you can find an example |
9797
| resolveDNSHost (**optional**) | Function that resolves the final address of the detected/parsed URL to prevent SSRF attacks |
98+
| onResponse (**optional**) | Function that handles the response object to allow for managing special cases |
9899

99100
```javascript
100101
getLinkPreview("https://www.youtube.com/watch?v=MejbOFk7H6c", {
@@ -159,6 +160,25 @@ await getLinkPreview(`http://google.com/`, {
159160
});
160161
```
161162

163+
## onResponse Use Cases
164+
There may be situations where you need to provide your own logic for population properties in the response object. For example, if the library is unable to detect a description because the website does not provide OpenGraph data, you might want to use the text value of the first paragraph instead. This callback gives you access to the Cheerio doc instance, as well as the URL object so you could handle cases on a site-by-site basis, if you need to. This callback must return the modified response object
165+
166+
```javascript
167+
await getLinkPreview(`https://example.com/`, {
168+
onResponse: (response, doc, URL) => {
169+
if (URL.hostname == 'example.com') {
170+
response.siteName = 'Example Website';
171+
}
172+
173+
if (!response.description) {
174+
response.description = doc('p').first().text();
175+
}
176+
177+
return response;
178+
},
179+
});
180+
```
181+
162182
## Response
163183

164184
Returns a Promise that resolves with an object describing the provided link.

__tests__/index.spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,23 @@ describe(`#getLinkPreview()`, () => {
243243
expect(response.mediaType).toEqual(`website`);
244244
});
245245

246+
it("should handle override response body using onResponse option", async () => {
247+
let firstParagraphText;
248+
249+
const res: any = await getLinkPreview(`https://www.example.com/`, {
250+
onResponse: (result, doc) => {
251+
firstParagraphText = doc('p').first().text().split('\n').map(x=> x.trim()).join(' ');
252+
result.siteName = `SiteName has been overridden`;
253+
result.description = firstParagraphText;
254+
255+
return result;
256+
}
257+
});
258+
259+
expect(res.siteName).toEqual("SiteName has been overridden");
260+
expect(res.description).toEqual(firstParagraphText);
261+
});
262+
246263
it("should handle video tags without type or secure_url tags", async () => {
247264
const res: any = await getLinkPreview(
248265
`https://newpathtitle.com/falling-markets-how-to-stop-buyer-from-getting-out/`,

index.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,26 @@ import cheerio from "cheerio";
22
import urlObj from "url";
33
import { CONSTANTS } from "./constants";
44

5+
interface ILinkPreviewResponse {
6+
url: string;
7+
title: string;
8+
siteName: string | undefined;
9+
description: string | undefined;
10+
mediaType: string;
11+
contentType: string | undefined;
12+
images: string[];
13+
videos: IVideoType[];
14+
favicons: string[];
15+
}
16+
17+
interface IVideoType {
18+
url: string | undefined,
19+
secureUrl: string | null | undefined,
20+
type: string | null | undefined,
21+
width: string | undefined,
22+
height: string | undefined,
23+
};
24+
525
interface ILinkPreviewOptions {
626
headers?: Record<string, string>;
727
imagesPropertyType?: string;
@@ -10,6 +30,7 @@ interface ILinkPreviewOptions {
1030
followRedirects?: `follow` | `error` | `manual`;
1131
resolveDNSHost?: (url: string) => Promise<string>;
1232
handleRedirects?: (baseURL: string, forwardedURL: string) => boolean;
33+
onResponse?: (response: ILinkPreviewResponse, doc: cheerio.Root, url?: URL) => ILinkPreviewResponse;
1334
}
1435

1536
interface IPreFetchedResource {
@@ -272,10 +293,10 @@ function parseTextResponse(
272293
url: string,
273294
options: ILinkPreviewOptions = {},
274295
contentType?: string
275-
) {
296+
): ILinkPreviewResponse {
276297
const doc = cheerio.load(body);
277298

278-
return {
299+
let response = {
279300
url,
280301
title: getTitle(doc),
281302
siteName: getSiteName(doc),
@@ -286,6 +307,23 @@ function parseTextResponse(
286307
videos: getVideos(doc),
287308
favicons: getFavicons(doc, url),
288309
};
310+
311+
if (options?.onResponse && typeof options.onResponse !== `function`) {
312+
throw new Error(
313+
`link-preview-js onResponse option must be a function`
314+
);
315+
}
316+
317+
if (options?.onResponse) {
318+
// send in a cloned response (to avoid mutation of original response reference)
319+
const clonedResponse = structuredClone(response);
320+
const urlObject = new URL(url)
321+
response = options.onResponse(clonedResponse, doc, urlObject);
322+
}
323+
324+
325+
return response;
326+
289327
}
290328

291329
function parseUnknownResponse(

0 commit comments

Comments
 (0)