@playpilot/tpi 6.11.0 → 7.0.0-beta.2

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/eslint.config.js CHANGED
@@ -18,6 +18,7 @@ export default [
18
18
  ...globals.browser,
19
19
  ...globals.node,
20
20
  PlayPilotLinkInjections: 'writable',
21
+ __SCRIPT_VERSION__: 'readonly',
21
22
  },
22
23
  parserOptions: {
23
24
  parser: tsParser,
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@playpilot/tpi",
3
- "version": "6.11.0",
3
+ "version": "7.0.0-beta.2",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
7
- "build": "vite build",
7
+ "dev-build": "npx http-server . /build.html -p 3000",
8
+ "build": "vite build --config vite._main.config.js && vite build --config vite._mount.config.js",
8
9
  "preview": "vite preview",
9
10
  "check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
10
11
  "check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
@@ -1,6 +1,4 @@
1
1
  import { getApiToken } from '$lib/token'
2
- import { TrackingEvent } from '../enums/TrackingEvent'
3
- import { track } from '../tracking'
4
2
  import { api } from './api'
5
3
 
6
4
  const cookieName = 'EncryptedToken'
@@ -28,12 +26,10 @@ export async function authorize(href: string = window.location.href): Promise<bo
28
26
  return true
29
27
  } catch(error: any) {
30
28
  console.error(error)
31
- track(TrackingEvent.AuthFailed)
29
+ throw error
32
30
  } finally {
33
31
  removeAuthParamFromUrl()
34
32
  }
35
-
36
- return false
37
33
  }
38
34
 
39
35
  export function getAuthToken(href: string = ''): string {
@@ -1,6 +1,6 @@
1
1
  import { authorize, getAuthToken, isEditorialModeEnabled } from './auth'
2
2
  import { generateRandomHash, stringToHash } from '../hash'
3
- import { getLanguage } from '../localization'
3
+ import { getLanguage } from '../language'
4
4
  import { getDatetime, getPageMetaData } from '../meta'
5
5
  import type { LinkInjectionResponse, LinkInjection } from '../types/injection'
6
6
  import { getFullUrlPath } from '../url'
@@ -1,4 +1,4 @@
1
- import { getLanguage } from '$lib/localization'
1
+ import { getLanguage } from '$lib/language'
2
2
  import { getApiToken } from '$lib/token'
3
3
  import type { APIPaginatedResult } from '$lib/types/api'
4
4
  import { paramsToString } from '$lib/url'
@@ -9,97 +9,6 @@ import { clearInTextDisclaimer, insertInTextDisclaimer } from './disclaimer'
9
9
  export const keyDataAttribute = 'data-playpilot-injection-key'
10
10
  export const keySelector = `[${keyDataAttribute}]`
11
11
 
12
- /**
13
- * Return a list of all valid text containing elements that may get injected into.
14
- * This excludes duplicates, empty elements, links, buttons, and header tags.
15
- *
16
- * Elements can additionally be excluded via the excludeElementsSelector attribute.
17
- * This will exclude any element that matches or is in that selector.
18
- */
19
- export function getLinkInjectionElements(parentElement: HTMLElement, excludeElementsSelector: string = ''): HTMLElement[] {
20
- const validElements: HTMLElement[] = []
21
- const remainingChildren = [parentElement]
22
-
23
- while (remainingChildren.length > 0) {
24
- const element = remainingChildren.pop() as HTMLElement
25
-
26
- if (validElements.includes(element)) continue
27
- if (excludeElementsSelector && element.matches(excludeElementsSelector)) continue
28
-
29
- // Ignore links, buttons, and headers
30
- if (/^(A|BUTTON|SCRIPT|NOSCRIPT|STYLE|IFRAME|FIGCAPTION|TIME|LABEL|BLOCKQUOTE|H1)$/.test(element.tagName)) continue
31
-
32
- // Ignore elements that are visibly hidden as they are likely only for screen readers.
33
- // These elements can be hidden via display: none or via a tiny width or perhaps even a clip path.
34
- // Checking by their offsetWidth seems like the surest way to ignore these elements.
35
- // We continue regardless of whether this is true or not, as we'd otherwise loop through children
36
- // which are also hidden because of their parent.
37
- // We always do it when running tests, as offsetWidth can't be reliably tested.
38
- if (process.env.NODE_ENV !== 'test' && element.offsetWidth <= 1) continue
39
-
40
- const hasTextNode = Array.from(element.childNodes).some(
41
- node => node.nodeType === Node.TEXT_NODE && node.nodeValue?.trim() !== '',
42
- )
43
-
44
- // Treat list items as a single element, via their list element parent.
45
- // This is to prevent missing out on list items with very short titles, such as lists
46
- // of movie titles with no other content in the list item. Sentences often contain the full
47
- // list, but breaking them up in separate list item elements means they can never get
48
- // injected into properly.
49
- if (hasTextNode && element.tagName === 'LI') {
50
- const listContainer = element.closest('ul, ol') as HTMLElement | null
51
-
52
- if (listContainer && !validElements.includes(listContainer)) {
53
- validElements.push(listContainer)
54
- }
55
-
56
- continue
57
- }
58
-
59
- // Some wysiwyg editors wrap contents of a paragraph in `span` elements when they contain styling.
60
- // This leads to broken up sentences which don't match correctly against the given sentence.
61
- // `<p><span>Some text</span><span> <strong>with broken up</strong></span><span>elements</span></p>`
62
- const isParagraphWithText = element.tagName === 'P' && !!element.textContent
63
-
64
- // If this element has a text node we add it to the valid elements and stop there
65
- if (hasTextNode || isParagraphWithText) {
66
- validElements.push(element)
67
- continue
68
- }
69
-
70
- // Add all children of the current element to be checked in this same loop.
71
- const children = Array.from(element.children) as HTMLElement[]
72
- for (let i = children.length - 1; i >= 0; i--) {
73
- remainingChildren.push(children[i] as HTMLElement)
74
- }
75
- }
76
-
77
- return validElements
78
- }
79
-
80
- /**
81
- * Get the parent selector that will be used to find the link injections in.
82
- * This selector is passed through the api config, or when the script is initialized.
83
- * If no selector is passed a default is returned instead.
84
- */
85
- export function getLinkInjectionsParentElement(): HTMLElement {
86
- const selector = window.PlayPilotLinkInjections?.selector
87
-
88
- if (selector) {
89
- // Replace : with \\: because js selectors need some characters escaped. We only care about : at the moment.
90
- const escaped = selector.replace(/(:)/g, '\\$1')
91
- const element = document.querySelector(escaped)
92
-
93
- if (element) return element as HTMLElement
94
- }
95
-
96
- return document.querySelector('article') || document.querySelector('main') || document.body
97
- }
98
-
99
- export function getPageText(elements: HTMLElement[]): string {
100
- return elements.map(element => element.innerText).join('\n\n')
101
- }
102
-
103
12
  /**
104
13
  * Replace all found injections within all given elements on the page
105
14
  * @returns Returns an array of injections with injections that failed to be inserted marked as `failed`.
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Return a list of all valid text containing elements that may get injected into.
3
+ * This excludes duplicates, empty elements, links, buttons, and header tags.
4
+ *
5
+ * Elements can additionally be excluded via the excludeElementsSelector attribute.
6
+ * This will exclude any element that matches or is in that selector.
7
+ */
8
+ export function getLinkInjectionElements(parentElement: HTMLElement, excludeElementsSelector: string = ''): HTMLElement[] {
9
+ const validElements: HTMLElement[] = []
10
+ const remainingChildren = [parentElement]
11
+
12
+ while (remainingChildren.length > 0) {
13
+ const element = remainingChildren.pop() as HTMLElement
14
+
15
+ if (validElements.includes(element)) continue
16
+ if (excludeElementsSelector && element.matches(excludeElementsSelector)) continue
17
+
18
+ // Ignore links, buttons, and headers
19
+ if (/^(A|BUTTON|SCRIPT|NOSCRIPT|STYLE|IFRAME|FIGCAPTION|TIME|LABEL|BLOCKQUOTE|H1)$/.test(element.tagName)) continue
20
+
21
+ // Ignore elements that are visibly hidden as they are likely only for screen readers.
22
+ // These elements can be hidden via display: none or via a tiny width or perhaps even a clip path.
23
+ // Checking by their offsetWidth seems like the surest way to ignore these elements.
24
+ // We continue regardless of whether this is true or not, as we'd otherwise loop through children
25
+ // which are also hidden because of their parent.
26
+ // We always do it when running tests, as offsetWidth can't be reliably tested.
27
+ if (process.env.NODE_ENV !== 'test' && element.offsetWidth <= 1) continue
28
+
29
+ const hasTextNode = Array.from(element.childNodes).some(
30
+ node => node.nodeType === Node.TEXT_NODE && node.nodeValue?.trim() !== '',
31
+ )
32
+
33
+ // Treat list items as a single element, via their list element parent.
34
+ // This is to prevent missing out on list items with very short titles, such as lists
35
+ // of movie titles with no other content in the list item. Sentences often contain the full
36
+ // list, but breaking them up in separate list item elements means they can never get
37
+ // injected into properly.
38
+ if (hasTextNode && element.tagName === 'LI') {
39
+ const listContainer = element.closest('ul, ol') as HTMLElement | null
40
+
41
+ if (listContainer && !validElements.includes(listContainer)) {
42
+ validElements.push(listContainer)
43
+ }
44
+
45
+ continue
46
+ }
47
+
48
+ // Some wysiwyg editors wrap contents of a paragraph in `span` elements when they contain styling.
49
+ // This leads to broken up sentences which don't match correctly against the given sentence.
50
+ // `<p><span>Some text</span><span> <strong>with broken up</strong></span><span>elements</span></p>`
51
+ const isParagraphWithText = element.tagName === 'P' && !!element.textContent
52
+
53
+ // If this element has a text node we add it to the valid elements and stop there
54
+ if (hasTextNode || isParagraphWithText) {
55
+ validElements.push(element)
56
+ continue
57
+ }
58
+
59
+ // Add all children of the current element to be checked in this same loop.
60
+ const children = Array.from(element.children) as HTMLElement[]
61
+ for (let i = children.length - 1; i >= 0; i--) {
62
+ remainingChildren.push(children[i] as HTMLElement)
63
+ }
64
+ }
65
+
66
+ return validElements
67
+ }
68
+
69
+ /**
70
+ * Get the parent selector that will be used to find the link injections in.
71
+ * This selector is passed through the api config, or when the script is initialized.
72
+ * If no selector is passed a default is returned instead.
73
+ */
74
+ export function getLinkInjectionsParentElement(): HTMLElement {
75
+ const selector = window.PlayPilotLinkInjections?.selector
76
+
77
+ if (selector) {
78
+ // Replace : with \\: because js selectors need some characters escaped. We only care about : at the moment.
79
+ const escaped = selector.replace(/(:)/g, '\\$1')
80
+ const element = document.querySelector(escaped)
81
+
82
+ if (element) return element as HTMLElement
83
+ }
84
+
85
+ return document.querySelector('article') || document.querySelector('main') || document.body
86
+ }
87
+
88
+ export function getPageText(elements: HTMLElement[]): string {
89
+ return elements.map(element => element.innerText).join('\n\n')
90
+ }
@@ -0,0 +1,25 @@
1
+ import { Language } from './enums/Language'
2
+ import type { LanguageCode } from './types/language'
3
+
4
+ export function getLanguage(): LanguageCode {
5
+ const configLanguage = window.PlayPilotLinkInjections?.language as LanguageCode | undefined
6
+ const languageCodes = Object.values(Language) as LanguageCode[]
7
+
8
+ if (configLanguage) {
9
+ if (languageCodes.includes(configLanguage)) return configLanguage
10
+
11
+ console.warn(`PlayPilot Link Injections: ${configLanguage} is not an accepted language`)
12
+ }
13
+
14
+ const documentLanguage = document.querySelector('html')?.getAttribute('lang')
15
+
16
+ if (documentLanguage) {
17
+ if (languageCodes.includes(documentLanguage as LanguageCode)) return documentLanguage as LanguageCode
18
+
19
+ // Match against shorthand language codes like da -> da-DK
20
+ const matched = languageCodes.find(code => code.toLowerCase().startsWith(documentLanguage + '-'))
21
+ if (matched) return matched
22
+ }
23
+
24
+ return Language.English
25
+ }
@@ -1,31 +1,8 @@
1
1
  import { translations } from './data/translations'
2
- import { Language } from './enums/Language'
2
+ import { getLanguage } from './language'
3
3
  import type { LanguageCode } from './types/language'
4
4
 
5
5
  export function t(key: string, language: LanguageCode = getLanguage()): string {
6
6
  // @ts-ignore It's fine if it's undefined
7
7
  return translations[key]?.[language] || key
8
8
  }
9
-
10
- export function getLanguage(): LanguageCode {
11
- const configLanguage = window.PlayPilotLinkInjections?.language as LanguageCode | undefined
12
- const languageCodes = Object.values(Language) as LanguageCode[]
13
-
14
- if (configLanguage) {
15
- if (languageCodes.includes(configLanguage)) return configLanguage
16
-
17
- console.warn(`PlayPilot Link Injections: ${configLanguage} is not an accepted language`)
18
- }
19
-
20
- const documentLanguage = document.querySelector('html')?.getAttribute('lang')
21
-
22
- if (documentLanguage) {
23
- if (languageCodes.includes(documentLanguage as LanguageCode)) return documentLanguage as LanguageCode
24
-
25
- // Match against shorthand language codes like da -> da-DK
26
- const matched = languageCodes.find(code => code.toLowerCase().startsWith(documentLanguage + '-'))
27
- if (matched) return matched
28
- }
29
-
30
- return Language.English
31
- }
package/src/lib/meta.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { getLinkInjectionsParentElement } from './injection'
1
+ import { getLinkInjectionsParentElement } from './injectionElements'
2
2
  import type { ArticleMetaData } from './types/injection'
3
3
 
4
4
  export function getPageMetaData(): ArticleMetaData {
@@ -36,7 +36,6 @@ export async function track(event: string, title: TitleData | null = null, paylo
36
36
  }
37
37
  }
38
38
 
39
- // eslint-disable-next-line no-undef
40
39
  payload.version = __SCRIPT_VERSION__
41
40
  payload.url = getFullUrlPath()
42
41
  payload.organization_sid = window.PlayPilotLinkInjections?.organization_sid || 'undefined'
@@ -4,10 +4,13 @@ declare global {
4
4
  interface Window {
5
5
  PlayPilotLinkInjections: ScriptConfig & {
6
6
  app: any | null
7
- initialize(config: ScriptConfig): void
7
+ initialize(config: ScriptConfig): Promise<void>
8
+ destroy(): void
9
+ mount(): void
10
+ }
11
+ PlayPilotMount: {
12
+ mount(): void
8
13
  destroy(): void
9
- debug(): void
10
- mockAd(override?: Record<any, any> = {}): void
11
14
  }
12
15
  }
13
16
  }
@@ -1,6 +1,6 @@
1
1
  import type { Campaign } from './campaign'
2
2
  import type { ConsentOptions } from './consent'
3
- import type { LinkInjection } from './injection'
3
+ import type { LinkInjection, LinkInjectionResponse } from './injection'
4
4
  import type { TitleData } from './title'
5
5
 
6
6
  export type ScriptConfig = {
@@ -30,6 +30,8 @@ export type ScriptConfig = {
30
30
  split_test_identifiers?: Record<string, number>
31
31
  // All link injections as returned from external-pages, with AI and manual injections merged.
32
32
  evaluated_link_injections?: LinkInjection[]
33
+ // Initial link injections as fetched from the main script.
34
+ initial_link_injections_promise?: Promise<LinkInjectionResponse> | null
33
35
  // By default the script requires consent from the user through tcfapi. Can be disabled using this setting.
34
36
  require_consent?: boolean
35
37
  // Used to check if a user has consented via tcfapi to various consent categories.
package/src/main.ts CHANGED
@@ -1,9 +1,8 @@
1
- import { mount } from 'svelte'
2
- import App from './routes/+page.svelte'
3
- import { clearLinkInjections, getLinkInjectionElements, getLinkInjectionsParentElement, getPageText } from '$lib/injection'
4
- import { getPageMetaData } from '$lib/meta'
1
+ import { fetchConfig } from '$lib/api/config'
2
+ import { pollLinkInjections } from '$lib/api/externalPages'
5
3
  import { setConsent } from '$lib/consent'
6
- import type { Campaign } from '$lib/types/campaign'
4
+ import { isCrawler } from '$lib/crawler'
5
+ import { getLinkInjectionElements, getLinkInjectionsParentElement, getPageText } from '$lib/injectionElements'
7
6
 
8
7
  window.PlayPilotLinkInjections = {
9
8
  token: '',
@@ -20,6 +19,7 @@ window.PlayPilotLinkInjections = {
20
19
  queued_tracking_events: [],
21
20
  split_test_identifiers: {},
22
21
  evaluated_link_injections: [],
22
+ initial_link_injections_promise: null,
23
23
  ads: [],
24
24
  require_consent: true,
25
25
  consents: {
@@ -32,7 +32,7 @@ window.PlayPilotLinkInjections = {
32
32
  config: {},
33
33
  app: null,
34
34
 
35
- initialize(options = { token: '', selector: '', after_article_selector: '', after_article_insert_position: '', language: null, region: null, organization_sid: null, domain_sid: null, editorial_token: '', require_consent: true }): void {
35
+ async initialize(options = { token: '', selector: '', after_article_selector: '', after_article_insert_position: '', language: null, region: null, organization_sid: null, domain_sid: null, editorial_token: '', require_consent: true }): Promise<void> {
36
36
  if (!options.token) {
37
37
  console.error('An API token is required.')
38
38
  return
@@ -60,96 +60,32 @@ window.PlayPilotLinkInjections = {
60
60
  })
61
61
  }
62
62
 
63
- if (this.app) this.destroy()
63
+ this.config = await fetchConfig() || {}
64
64
 
65
- const target = document.createElement('div')
66
- target.id = 'playpilot-link-injection'
67
-
68
- document.body.insertAdjacentElement('beforeend', target)
65
+ const parentElement = getLinkInjectionsParentElement()
66
+ const elements = getLinkInjectionElements(parentElement, this.config.exclude_elements_selector || '')
67
+ const pageText = getPageText(elements)
69
68
 
70
- this.app = mount(App, { target })
69
+ this.initial_link_injections_promise = pollLinkInjections(pageText, { maxTries: 1, runAiWhenRelevant: !isCrawler() })
70
+ this.mount()
71
71
  },
72
72
 
73
73
  destroy(): void {
74
- if (!this.app) return
75
-
76
- this.app = null
77
-
78
- const target = document.getElementById('playpilot-link-injection')
79
- if (target) target.remove()
80
-
81
- clearLinkInjections()
74
+ window.PlayPilotMount?.destroy()
82
75
  },
83
76
 
84
- debug(): void {
85
- const parentElement = getLinkInjectionsParentElement()
86
- const elements = getLinkInjectionElements(parentElement)
87
-
88
- console.groupCollapsed('Config')
89
- console.table(Object.entries(this))
90
- console.groupEnd()
91
-
92
- console.groupCollapsed('Elements')
93
- console.log('Parent element', parentElement)
94
- console.log('Valid elements', elements)
95
- console.groupEnd()
77
+ mount(): void {
78
+ const script = document.createElement('script')
96
79
 
97
- console.groupCollapsed('Last fetch')
98
- console.log(this.last_successful_fetch)
99
- console.groupEnd()
100
-
101
- console.groupCollapsed('Meta')
102
- console.log(getPageMetaData())
103
- console.groupEnd()
104
-
105
- console.groupCollapsed('Page text')
106
- console.log(getPageText(elements))
107
- console.groupEnd()
108
-
109
- console.groupCollapsed('Evaluated injections')
110
- console.log(this.evaluated_link_injections)
111
- console.groupEnd()
112
-
113
- console.groupCollapsed('Tracked events')
114
- console.log(this.tracked_events)
115
- console.groupEnd()
116
-
117
- console.groupCollapsed('Queued tracking events')
118
- console.log(this.queued_tracking_events)
119
- console.groupEnd()
120
-
121
- console.groupCollapsed('Split tests')
122
- console.log(this.split_test_identifiers)
123
- console.groupEnd()
124
- },
80
+ script.src = `https://cdn.jsdelivr.net/npm/@playpilot/tpi@${__SCRIPT_VERSION__}/dist/mount.js`
81
+ // script.src = './dist/mount.js' // Use me during development of this script
125
82
 
126
- mockAd(override = {}): void {
127
- /** @ts-ignore We're not including every field here, that's ok. */
128
- const campaign: Campaign = {
129
- campaign_format: 'card',
130
- campaign_type: 'image',
131
- campaign_name: 'some_campaign',
132
- content: {
133
- header: 'Some content header',
134
- header_logo: 'https://picsum.photos/seed/picsum/40/40',
135
- header_logo_uuid: '',
136
- subheader: 'Some content subheader',
137
- image: 'https://picsum.photos/seed/picsum/640/360',
138
- image_uuid: '',
139
- video: null,
140
- format: null,
141
- },
142
- cta: {
143
- header: 'Some cta header',
144
- subheader: 'Some cta subheader',
145
- url: 'https://google.com/',
146
- image: null,
147
- image_uuid: null,
148
- },
149
- disclaimer: 'Some disclaimer',
83
+ script.onload = () => {
84
+ console.log('on load')
85
+ window.PlayPilotMount?.mount()
150
86
  }
151
87
 
152
- this.ads = [{ ...campaign, ...override }]
88
+ document.body.insertAdjacentElement('beforeend', script)
153
89
  },
154
90
  }
155
91
 
package/src/mount.ts ADDED
@@ -0,0 +1,29 @@
1
+ import { mount } from 'svelte'
2
+ import App from './routes/+page.svelte'
3
+ import { clearLinkInjections } from '$lib/injection'
4
+
5
+ window.PlayPilotMount = {
6
+ mount(): void {
7
+ if (window.PlayPilotLinkInjections.app) this.destroy()
8
+
9
+ const target = document.createElement('div')
10
+ target.id = 'playpilot-link-injection'
11
+
12
+ document.body.insertAdjacentElement('beforeend', target)
13
+
14
+ window.PlayPilotLinkInjections.app = mount(App, { target })
15
+ },
16
+
17
+ destroy(): void {
18
+ if (!window.PlayPilotLinkInjections) return
19
+
20
+ window.PlayPilotLinkInjections.app = null
21
+
22
+ const target = document.getElementById('playpilot-link-injection')
23
+ if (target) target.remove()
24
+
25
+ clearLinkInjections()
26
+ },
27
+ }
28
+
29
+ export default window.PlayPilotLinkInjections
@@ -1,7 +1,8 @@
1
1
  <script lang="ts">
2
2
  import { onDestroy } from 'svelte'
3
3
  import { pollLinkInjections } from '$lib/api/externalPages'
4
- import { clearLinkInjections, getLinkInjectionElements, getLinkInjectionsParentElement, getPageText, injectLinksInDocument, separateLinkInjectionTypes } from '$lib/injection'
4
+ import { clearLinkInjections, injectLinksInDocument, separateLinkInjectionTypes } from '$lib/injection'
5
+ import { getLinkInjectionElements, getLinkInjectionsParentElement, getPageText } from '$lib/injectionElements'
5
6
  import { fireQueuedTrackingEvents, setTrackingSids, track } from '$lib/tracking'
6
7
  import { getFullUrlPath } from '$lib/url'
7
8
  import { isCrawler } from '$lib/crawler'
@@ -65,13 +66,16 @@
65
66
  async function initialize(): Promise<void> {
66
67
  loading = true
67
68
 
68
- if (isEditorialMode) authorized = await authorize()
69
+ try {
70
+ if (isEditorialMode) authorized = await authorize()
71
+ } catch {
72
+ track(TrackingEvent.AuthFailed)
73
+ }
69
74
 
70
75
  try {
71
- const config = await fetchConfig()
72
76
  const url = getFullUrlPath()
73
77
 
74
- window.PlayPilotLinkInjections.config = config
78
+ const config = window.PlayPilotLinkInjections.config || await fetchConfig()
75
79
 
76
80
  if (config?.custom_style) insertCustomStyle(config.custom_style || '')
77
81
  if (config?.explore_navigation_selector) insertExploreIntoNavigation()
@@ -89,9 +93,8 @@
89
93
  }
90
94
 
91
95
  try {
92
- // Only trying once when not in editorial mode to prevent late injections (as well as a ton of requests)
93
- // by users who are not in the editorial view.
94
- response = await pollLinkInjections(pageText, { maxTries: 1, runAiWhenRelevant: !isCrawler() })
96
+ response = await window.PlayPilotLinkInjections.initial_link_injections_promise || null
97
+ response ||= await pollLinkInjections(pageText, { maxTries: 1, runAiWhenRelevant: !isCrawler() })
95
98
 
96
99
  loading = false
97
100
 
@@ -13,7 +13,8 @@
13
13
  import type { LinkInjection } from '$lib/types/injection'
14
14
  import type { TitleData } from '$lib/types/title'
15
15
  import { cleanPhrase, truncateAroundPhrase } from '$lib/text'
16
- import { getLinkInjectionElements, getLinkInjectionsParentElement, isValidPlaylinkType } from '$lib/injection'
16
+ import { isValidPlaylinkType } from '$lib/injection'
17
+ import { getLinkInjectionElements, getLinkInjectionsParentElement } from '$lib/injectionElements'
17
18
  import { imagePlaceholderDataUrl } from '$lib/constants'
18
19
  import { removeImageUrlPrefix } from '$lib/image'
19
20
  import ReportIssueModal from './ReportIssueModal.svelte'
@@ -4,7 +4,7 @@
4
4
  import { onMount } from 'svelte'
5
5
  import { playPilotBaseUrl } from '$lib/constants'
6
6
  import { generateInjectionKey } from '$lib/api/externalPages'
7
- import { getLinkInjectionsParentElement } from '$lib/injection'
7
+ import { getLinkInjectionsParentElement } from '$lib/injectionElements'
8
8
  import { heading } from '$lib/actions/heading'
9
9
  import { findSurroundingPhrases, cleanPhrase } from '$lib/text'
10
10
  import { getIndexOfSelection } from '$lib/selection'
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { fetchParticipantsForTitle } from '$lib/api/participants'
3
3
  import { TrackingEvent } from '$lib/enums/TrackingEvent'
4
- import { getLinkInjectionElements, getLinkInjectionsParentElement, getPageText } from '$lib/injection'
4
+ import { getLinkInjectionElements, getLinkInjectionsParentElement, getPageText } from '$lib/injectionElements'
5
5
  import { t } from '$lib/localization'
6
6
  import { openModal } from '$lib/modal'
7
7
  import { cleanPhrase, findNumberOfMatchesInString } from '$lib/text'