-
Notifications
You must be signed in to change notification settings - Fork 528
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(core): expose metadata of widgets (#4604)
* feat(core): expose widgetParams to the head on crawler when the user agent of the crawler is detected a meta tag is added to the document head with the widget parameters for the added widgets. Co-authored-by: François Chalifour <[email protected]> Co-authored-by: Eunjae Lee <[email protected]> Co-authored-by: Yannick Croissant <[email protected]> Co-authored-by: Clément Vannicatte <[email protected]> * use newest helper * fix eslint * simplify * remove unused $$params * extract payload recursively & safer * fix(index): add official widget disclaimer * chore: consistent import * test(telemetry): add unitary test * rename officialWidget to official * remove $$official (for $$widgetType in separate PR) * move DOM modification to subscribe callback * move telemetry enabled * fix capitalisation * no fallback for type (crawler does it) * rename to metadata * test(InstantSearch): assert that middleware gets added in right condition * create meta tag on middleware creation instead of "creator" * add comment * chore: use empty scopedResults this prevents it from being classed as a response Co-authored-by: François Chalifour <[email protected]> Co-authored-by: Eunjae Lee <[email protected]> Co-authored-by: Yannick Croissant <[email protected]> Co-authored-by: Clément Vannicatte <[email protected]>
- Loading branch information
1 parent
8feef58
commit 1fcf716
Showing
34 changed files
with
651 additions
and
271 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
import { createMetadataMiddleware } from '..'; | ||
import { createSearchClient } from '../../../test/mock/createSearchClient'; | ||
import instantsearch from '../../lib/main'; | ||
import { configure, hits, index, pagination, searchBox } from '../../widgets'; | ||
import { isMetadataEnabled } from '../createMetadataMiddleware'; | ||
|
||
declare global { | ||
// using namespace so it's only in this file | ||
// eslint-disable-next-line @typescript-eslint/no-namespace | ||
namespace NodeJS { | ||
interface Global { | ||
navigator: { | ||
userAgent: string; | ||
}; | ||
window: Window; | ||
} | ||
} | ||
} | ||
|
||
const { window } = global; | ||
Object.defineProperty( | ||
window.navigator, | ||
'userAgent', | ||
(value => ({ | ||
get() { | ||
return value; | ||
}, | ||
set(v: string) { | ||
value = v; | ||
}, | ||
}))(window.navigator.userAgent) | ||
); | ||
|
||
const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); | ||
|
||
const defaultUserAgent = | ||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1 Safari/605.1.15'; | ||
const algoliaUserAgent = 'Algolia Crawler 5.3.2'; | ||
|
||
describe('createMetadataMiddleware', () => { | ||
beforeEach(() => { | ||
document.head.innerHTML = ''; | ||
}); | ||
|
||
describe('metadata disabled', () => { | ||
it('does not enable on normal user agent', () => { | ||
global.navigator.userAgent = defaultUserAgent; | ||
|
||
expect(isMetadataEnabled()).toBe(false); | ||
}); | ||
|
||
it("does not enable when there's no window", () => { | ||
global.navigator.userAgent = algoliaUserAgent; | ||
|
||
// @ts-ignore | ||
delete global.window; | ||
|
||
createMetadataMiddleware(); | ||
|
||
expect(isMetadataEnabled()).toBe(false); | ||
|
||
global.window = window; | ||
}); | ||
}); | ||
|
||
describe('metadata enabled', () => { | ||
beforeEach(() => { | ||
global.navigator.userAgent = algoliaUserAgent; | ||
}); | ||
|
||
it('metadata enabled returns true', () => { | ||
expect(isMetadataEnabled()).toBe(true); | ||
}); | ||
|
||
it('does not add meta before subscribe', () => { | ||
createMetadataMiddleware(); | ||
|
||
expect(document.head).toMatchInlineSnapshot(`<head />`); | ||
}); | ||
|
||
it('fills it with widgets after start', async () => { | ||
// not using createMetadataMiddleware() here, | ||
// since metadata is built into instantsearch | ||
const search = instantsearch({ | ||
searchClient: createSearchClient(), | ||
indexName: 'test', | ||
}); | ||
|
||
search.addWidgets([ | ||
searchBox({ container: document.createElement('div') }), | ||
searchBox({ container: document.createElement('div') }), | ||
hits({ container: document.createElement('div'), escapeHTML: true }), | ||
index({ indexName: 'test2' }).addWidgets([ | ||
pagination({ container: document.createElement('div') }), | ||
configure({ distinct: true, filters: 'hehe secret string!' }), | ||
]), | ||
]); | ||
|
||
search.start(); | ||
|
||
await wait(100); | ||
|
||
expect(document.head).toMatchInlineSnapshot(` | ||
<head> | ||
<meta | ||
content="{\\"widgets\\":[{\\"type\\":\\"ais.searchBox\\",\\"params\\":[]},{\\"type\\":\\"ais.searchBox\\",\\"params\\":[]},{\\"type\\":\\"ais.hits\\",\\"params\\":[\\"escapeHTML\\"]},{\\"type\\":\\"ais.index\\",\\"params\\":[]},{\\"type\\":\\"ais.pagination\\",\\"params\\":[]},{\\"type\\":\\"ais.configure\\",\\"params\\":[\\"searchParameters\\"]}]}" | ||
name="instantsearch:widgets" | ||
/> | ||
</head> | ||
`); | ||
|
||
expect(JSON.parse(document.head.querySelector('meta')!.content)) | ||
.toMatchInlineSnapshot(` | ||
Object { | ||
"widgets": Array [ | ||
Object { | ||
"params": Array [], | ||
"type": "ais.searchBox", | ||
}, | ||
Object { | ||
"params": Array [], | ||
"type": "ais.searchBox", | ||
}, | ||
Object { | ||
"params": Array [ | ||
"escapeHTML", | ||
], | ||
"type": "ais.hits", | ||
}, | ||
Object { | ||
"params": Array [], | ||
"type": "ais.index", | ||
}, | ||
Object { | ||
"params": Array [], | ||
"type": "ais.pagination", | ||
}, | ||
Object { | ||
"params": Array [ | ||
"searchParameters", | ||
], | ||
"type": "ais.configure", | ||
}, | ||
], | ||
} | ||
`); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
import { InstantSearch, Middleware, Widget } from '../types'; | ||
import { Index } from '../widgets/index/index'; | ||
|
||
type WidgetMetaData = { | ||
type: string | undefined; | ||
params: string[]; | ||
}; | ||
|
||
type Payload = { | ||
widgets: WidgetMetaData[]; | ||
}; | ||
|
||
function extractPayload( | ||
widgets: Array<Widget<{ renderState: any }>>, | ||
instantSearchInstance: InstantSearch, | ||
payload: Payload | ||
) { | ||
const parent = instantSearchInstance.mainIndex; | ||
|
||
const initOptions = { | ||
instantSearchInstance, | ||
parent, | ||
scopedResults: [], | ||
state: parent.getHelper()!.state, | ||
helper: parent.getHelper()!, | ||
createURL: parent.createURL, | ||
uiState: instantSearchInstance._initialUiState, | ||
renderState: instantSearchInstance.renderState, | ||
templatesConfig: instantSearchInstance.templatesConfig, | ||
searchMetadata: { | ||
isSearchStalled: instantSearchInstance._isSearchStalled, | ||
}, | ||
}; | ||
|
||
widgets.forEach(widget => { | ||
let widgetParams = {}; | ||
|
||
if (widget.getWidgetRenderState) { | ||
const renderState = widget.getWidgetRenderState(initOptions); | ||
|
||
if (renderState && renderState.widgetParams) { | ||
widgetParams = renderState.widgetParams; | ||
} | ||
} | ||
|
||
// since we destructure in all widgets, the parameters with defaults are set to "undefined" | ||
const params = Object.keys(widgetParams).filter( | ||
key => widgetParams[key] !== undefined | ||
); | ||
|
||
payload.widgets.push({ | ||
type: widget.$$type, | ||
params, | ||
}); | ||
|
||
if (widget.$$type === 'ais.index') { | ||
extractPayload( | ||
(widget as Index).getWidgets(), | ||
instantSearchInstance, | ||
payload | ||
); | ||
} | ||
}); | ||
} | ||
|
||
export function isMetadataEnabled() { | ||
return ( | ||
typeof window !== 'undefined' && | ||
window.navigator.userAgent.indexOf('Algolia Crawler') > -1 | ||
); | ||
} | ||
|
||
/** | ||
* Exposes the metadata of mounted widgets in a custom | ||
* `<meta name="instantsearch:widgets" />` tag. The metadata per widget is: | ||
* - applied parameters | ||
* - widget name | ||
* - connector name | ||
*/ | ||
export function createMetadataMiddleware(): Middleware { | ||
return ({ instantSearchInstance }) => { | ||
const payload: Payload = { | ||
widgets: [], | ||
}; | ||
const payloadContainer = document.createElement('meta'); | ||
const refNode = document.querySelector('head')!; | ||
payloadContainer.name = 'instantsearch:widgets'; | ||
|
||
return { | ||
onStateChange() {}, | ||
subscribe() { | ||
// using setTimeout here to delay extraction until widgets have been added in a tick (e.g. Vue) | ||
setTimeout(() => { | ||
extractPayload( | ||
instantSearchInstance.mainIndex.getWidgets(), | ||
instantSearchInstance, | ||
payload | ||
); | ||
|
||
payloadContainer.content = JSON.stringify(payload); | ||
refNode.appendChild(payloadContainer); | ||
}, 0); | ||
}, | ||
|
||
unsubscribe() { | ||
payloadContainer.parentNode?.removeChild(payloadContainer); | ||
}, | ||
}; | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
export * from './createInsightsMiddleware'; | ||
export * from './createRouterMiddleware'; | ||
export * from './createMetadataMiddleware'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.