Skip to content

Commit

Permalink
Merge pull request #719 from microlinkhq/next
Browse files Browse the repository at this point in the history
fix(logo-favicon): expose resolveFaviconUrl
  • Loading branch information
Kikobeats authored Jul 15, 2024
2 parents ec0de8b + 2078325 commit d7ea4eb
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 52 deletions.
34 changes: 17 additions & 17 deletions packages/metascraper-logo-favicon/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,31 @@ $ npm install metascraper-logo-favicon --save

#### options

##### google
##### favicon

Type: `boolean`<br>
Default: `true`

It enables logo resolution using Google API.
It tries to resolve `favicon.ico` of the url.

##### favicon
##### google

Type: `boolean`<br>
Default: `true`

It tries to resolve `favicon.ico` of the url.
It enables logo resolution using Google API.

##### rootFavicon
##### gotOpts

Type: `boolean`|`regexp`<br>
Default: `true`
Type: `object`

It tries to resolve `favicon.ico` of the url when the URL is a subdomain.
Any option provided here will passed to [got#options](https://github.com/sindresorhus/got#options).

##### keyvOpts

Type: `object`

Any option provided here will passed to [@keyvhq/memoize#options](https://github.com/microlinkhq/keyv/tree/master/packages/memoize#keyvoptions).

##### pickFn

Expand Down Expand Up @@ -68,17 +73,12 @@ Type: `function`

It will be used to determine if a favicon URL is valid.

##### gotOpts

Type: `object`

Any option provided here will passed to [got#options](https://github.com/sindresorhus/got#options).

##### keyvOpts
##### rootFavicon

Type: `object`
Type: `boolean`|`regexp`<br>
Default: `true`

Any option provided here will passed to [@keyvhq/memoize#options](https://github.com/microlinkhq/keyv/tree/master/packages/memoize#keyvoptions).
It tries to resolve `favicon.ico` of the url when the URL is a subdomain.

## License

Expand Down
20 changes: 15 additions & 5 deletions packages/metascraper-logo-favicon/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,38 @@ type Options = {
* @default true
*/
favicon?: boolean,
/**
* Enable favicon.ico using the root domain for subdomains
* @default true
*/
rootFavicon?: boolean | RegExp,

/**
* Enable retrieve logo from Google API.
* @default true
*/
google?: boolean,

/**
* https://github.com/sindresorhus/got#options
*/
gotOpts?: import('got').Options,

/**
* https://github.com/microlinkhq/keyv/tree/master/packages/memoize#keyvoptions
*/
keyvOpts?: import('@keyvhq/core').Options<any>,

/**
* The function to pick the favicon from the list of favicons.
*/
pickFn?: (sizes: DOMNOdeAtributes[]) => DOMNOdeAtributes,

/**
* It will be used to determine if a favicon URL is valid.
*/
resolveFaviconUrl?: (faviconUrl: string, contentTypes: string[], gotOpts: import('got').Options) => Promise<import('got').Response<string> | undefined>,

/**
* Enable favicon.ico using the root domain for subdomains
* @default true
*/
rootFavicon?: boolean | RegExp,
}

declare function rules(options?: Options): import('metascraper').Rules;
Expand Down
65 changes: 35 additions & 30 deletions packages/metascraper-logo-favicon/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,31 +97,22 @@ const sizeSelectors = [
{ tag: 'meta[name*="msapplication" i]', attr: 'content' } // Windows 8, Internet Explorer 11 Tiles
]

const firstReachable = async (domNodeSizes, gotOpts) => {
const firstReachable = async (domNodeSizes, resolveFaviconUrl, gotOpts) => {
for (const { url } of domNodeSizes) {
const response = await reachableUrl(url, gotOpts)
if (!reachableUrl.isReachable(response)) continue
const contentType = response.headers['content-type']

const urlExtension = extension(url)

const contentTypes = ALLOWED_EXTENSION_CONTENT_TYPES.find(
([ext]) => ext === urlExtension
)

if (
contentTypes &&
(!isValidContenType(contentType, contentTypes[1]) ||
response.body.toString()[0] === '<')
) {
continue
}

return response.url
const response = await resolveFaviconUrl(url, contentTypes, gotOpts)
if (response !== undefined) return response.url
}
}

const pickBiggerSize = async (sizes, { gotOpts } = {}) => {
const pickBiggerSize = async (
sizes,
{ resolveFaviconUrl = defaultResolveFaviconUrl, gotOpts } = {}
) => {
const sorted = sizes.reduce(
(acc, item) => {
acc[item.size.square ? 'square' : 'nonSquare'].push(item)
Expand All @@ -131,8 +122,16 @@ const pickBiggerSize = async (sizes, { gotOpts } = {}) => {
)

return (
(await firstReachable(pickBiggerSize.sortBySize(sorted.square), gotOpts)) ||
(await firstReachable(pickBiggerSize.sortBySize(sorted.nonSquare), gotOpts))
(await firstReachable(
pickBiggerSize.sortBySize(sorted.square),
resolveFaviconUrl,
gotOpts
)) ||
(await firstReachable(
pickBiggerSize.sortBySize(sorted.nonSquare),
resolveFaviconUrl,
gotOpts
))
)
}

Expand All @@ -145,19 +144,15 @@ const defaultResolveFaviconUrl = async (faviconUrl, contentTypes, gotOpts) => {

const contentType = response.headers['content-type']

if (
contentTypes &&
(!isValidContenType(contentType, contentTypes) ||
response.body.toString()[0] === '<')
) {
if (contentTypes && !isValidContenType(contentType, contentTypes)) {
return undefined
}

if (contentTypes && !isValidContenType(contentType, contentTypes)) {
if (contentTypes && response.body.toString()[0] === '<') {
return undefined
}

return response.url
return response
}

const createFavicon = (
Expand All @@ -167,7 +162,9 @@ const createFavicon = (
return async (url, { gotOpts } = {}) => {
const faviconUrl = logo(`/favicon.${ext}`, { url })
return faviconUrl
? resolveFaviconUrl(faviconUrl, contentTypes, gotOpts)
? resolveFaviconUrl(faviconUrl, contentTypes, gotOpts).then(
response => response?.url
)
: undefined
}
}
Expand Down Expand Up @@ -224,14 +221,21 @@ const createRootFavicon = ({ getLogo, withRootFavicon = true } = {}) => {
}

module.exports = ({
google: withGoogle = true,
favicon: withFavicon = true,
rootFavicon: withRootFavicon = true,
google: withGoogle = true,
gotOpts,
keyvOpts,
pickFn = pickBiggerSize
pickFn = pickBiggerSize,
resolveFaviconUrl = defaultResolveFaviconUrl,
rootFavicon: withRootFavicon = true
} = {}) => {
const getLogo = createGetLogo({ withGoogle, withFavicon, gotOpts, keyvOpts })
const getLogo = createGetLogo({
gotOpts,
keyvOpts,
resolveFaviconUrl,
withFavicon,
withGoogle
})
const rootFavicon = createRootFavicon({ getLogo, withRootFavicon })
return {
logo: [
Expand All @@ -250,3 +254,4 @@ module.exports.createFavicon = createFavicon
module.exports.createRootFavicon = createRootFavicon
module.exports.createGetLogo = createGetLogo
module.exports.pickBiggerSize = pickBiggerSize
module.exports.resolveFaviconUrl = defaultResolveFaviconUrl
37 changes: 37 additions & 0 deletions packages/metascraper-logo-favicon/test/resolve-favicon-url.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use strict'

const test = require('ava')

const { resolveFaviconUrl } = require('..')
const { runServer } = require('./helpers')

const toUrl = (basename, pathname) => new URL(pathname, basename).toString()

test('undefined if favicon url is not reachable', async t => {
const url = await runServer(t, async ({ res }) => {
res.statusCode = 404
res.end('Not Found')
})
t.is(await resolveFaviconUrl(toUrl(url, 'favico.ico')), undefined)
})

test('undefined if content type is not expected', async t => {
const url = await runServer(t, async ({ res }) => {
res.statusCode = 200
res.setHeader('Content-Type', 'image/svg+xml')
res.end()
})
t.is(await resolveFaviconUrl(toUrl(url, 'favicon.ico'), []), undefined)
})

test('undefined if body is not the expected according to content type', async t => {
const url = await runServer(t, async ({ res }) => {
res.setHeader('content-type', 'image/x-icon')
res.end('<svg></svg>')
})

t.is(
await resolveFaviconUrl(`${url}favicon.png`, ['image/x-icon']),
undefined
)
})

0 comments on commit d7ea4eb

Please sign in to comment.