@playpilot/tpi 8.14.0-beta.4 → 8.15.0

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/events.md CHANGED
@@ -100,6 +100,11 @@ Event | Action | Info | Payload
100
100
  `ali_display_ad_playlink_click` | _Fires any time the playlink linked to the display ad is clicked_ | | `campaign_name`
101
101
  `ali_ads_fetch_failed` | _Fires whenever ads tried to but failed to fetch_ | | `status` (Response status code)
102
102
 
103
+ ## Widgets
104
+ Event | Action | Info | Payload
105
+ --- | --- | --- | ---
106
+ `ali_widget_article_rail_title_click` | _Fires when a title in an tpi rail widget is clicked_ | | `Title`
107
+
103
108
  ### Various
104
109
  Event | Action | Info | Payload
105
110
  --- | --- | --- | ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playpilot/tpi",
3
- "version": "8.14.0-beta.4",
3
+ "version": "8.15.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -36,7 +36,7 @@
36
36
  "svelte": "5.44.1",
37
37
  "svelte-check": "^4.0.0",
38
38
  "svelte-preprocess": "^6.0.3",
39
- "svelte-tiny-slider": "^2.7.1",
39
+ "svelte-tiny-slider": "^2.7.2",
40
40
  "typescript": "^5.9.3",
41
41
  "typescript-eslint": "^8.59.2",
42
42
  "vite": "^5.4.21",
@@ -26,8 +26,6 @@ export async function fetchLinkInjections(
26
26
  const apiUrl = `/external-pages/?api-token=${apiToken}&include_title_details=true${isEditorialMode ? '&editorial_mode_enabled=true' : ''}&language=${language}`
27
27
  let response: LinkInjectionResponse
28
28
 
29
- // We use separate requests when running the AI or setting the editor session vs when only getting the results.
30
- // For regular requests we use a GET endpoint, but when saving data we POST to the same url.
31
29
  if (method === 'POST') {
32
30
  const { pageText } = getPageTextAndElements()
33
31
 
@@ -44,14 +42,11 @@ export async function fetchLinkInjections(
44
42
  body: params,
45
43
  })
46
44
  } else {
47
- // When getting injections without posting we append the URL of the page to the URL for the request.
48
- // All other params are only relevant during the POST request.
49
45
  response = await api<LinkInjectionResponse>(apiUrl + `&url=${url}`, {
50
46
  method: 'GET',
51
47
  })
52
48
  }
53
49
 
54
- // This is used when debugging (using window.PlayPilotLinkInjections.debug())
55
50
  window.PlayPilotLinkInjections.last_successful_fetch = response
56
51
 
57
52
  return response
@@ -67,8 +62,6 @@ export async function pollLinkInjections(
67
62
  ): Promise<LinkInjectionResponse> {
68
63
  let currentTry = 0
69
64
 
70
- // Clear pollTimeout if it is already running to prevent multiple timeouts from running at the same time
71
- // This is mostly handy during HMR, but also during navigation changes
72
65
  if (pollTimeout) clearTimeout(pollTimeout)
73
66
 
74
67
  const poll = async (resolve: Function, reject: Function): Promise<void> => {
@@ -171,6 +171,11 @@ export const translations = {
171
171
  [Language.Swedish]: 'Upptäck och sök bland alla filmer och tv-serier',
172
172
  [Language.Danish]: 'Opdag og søg i alle film og tv-serier',
173
173
  },
174
+ 'Streaming Guide Disclaimer': {
175
+ [Language.English]: 'In collaboration with <a href="https://www.playpilot.com/" target="_blank" rel="sponsored">PlayPilot.com</a>',
176
+ [Language.Swedish]: 'I samarbete med <a href="https://www.playpilot.com/" target="_blank" rel="sponsored">PlayPilot.com</a>',
177
+ [Language.Danish]: 'I samarbejde med <a href="https://www.playpilot.com/" target="_blank" rel="sponsored">PlayPilot.com</a>',
178
+ },
174
179
  'Streaming Guide Description': {
175
180
  [Language.English]: 'Find where to watch movies online - the ultimate guide that helps you find the best movies and shows across streaming services.',
176
181
  [Language.Swedish]: 'Sök bland alla filmer och serier för att ta reda på var du kan streama dem',
@@ -286,6 +291,11 @@ export const translations = {
286
291
  [Language.Swedish]: 'Sidan hittades inte',
287
292
  [Language.Danish]: 'Siden blev ikke fundet',
288
293
  },
294
+ 'Mentioned In This Article': {
295
+ [Language.English]: 'Mentioned in this article',
296
+ [Language.Swedish]: 'Nämnda i den här artikeln',
297
+ [Language.Danish]: 'Nævnt i denne artikel',
298
+ },
289
299
 
290
300
  // List titles
291
301
  'List: Trending': {
@@ -55,6 +55,9 @@ export const TrackingEvent = {
55
55
  SplitTestView: 'ali_split_test_view',
56
56
  SplitTestAction: 'ali_split_test_action',
57
57
 
58
+ // Widgets
59
+ WidgetArticleRailTitleClick: 'ali_widget_article_rail_title_click',
60
+
58
61
  // Various
59
62
  ShareTitle: 'ali_share_title',
60
63
  SaveTitle: 'ali_save_title',
@@ -49,7 +49,7 @@ export const linkInjections: LinkInjection[] = [{
49
49
  sentence: 'In an interview with Epire Magazine, Quan reveals he quested starring in Love Hurts',
50
50
  playpilot_url: 'https://playpilot.com/movie/example/',
51
51
  key: 'some-key-1',
52
- title_details: title,
52
+ title_details: { ...title, sid: '1' },
53
53
  }, {
54
54
  sid: '2',
55
55
  title: 'The Long Kiss Goodnight',
@@ -57,7 +57,7 @@ export const linkInjections: LinkInjection[] = [{
57
57
  playpilot_url: 'https://playpilot.com/movie/example-2/',
58
58
  key: 'some-key-2',
59
59
  after_article: false,
60
- title_details: title,
60
+ title_details: { ...title, sid: '2' },
61
61
  }, {
62
62
  sid: '3',
63
63
  title: 'Nobody',
@@ -65,7 +65,7 @@ export const linkInjections: LinkInjection[] = [{
65
65
  playpilot_url: 'https://playpilot.com/movie/example-3/',
66
66
  key: 'some-key-3',
67
67
  after_article: true,
68
- title_details: title,
68
+ title_details: { ...title, sid: '3' },
69
69
  manual: false,
70
70
  }, {
71
71
  sid: '4',
@@ -0,0 +1,43 @@
1
+ import { mount, unmount } from 'svelte'
2
+ import type { LinkInjection } from './types/injection'
3
+ import InjectionsWidgetRail from '../routes/components/Widgets/InjectionsWidgetRail.svelte'
4
+
5
+ const widgets: Record<string, any> = {
6
+ 'tpi-rail': InjectionsWidgetRail,
7
+ }
8
+
9
+ export const inTextWidgetSelector = '[data-playpilot-widget]'
10
+
11
+ export let inTextWidgetInsertedComponents: any[] = []
12
+
13
+ export function insertInTextWidgets(linkInjections: LinkInjection[]): void {
14
+ clearInTextWidgets()
15
+
16
+ if (!linkInjections.length) return
17
+
18
+ const targets = document.querySelectorAll<HTMLElement>(inTextWidgetSelector)
19
+
20
+ targets.forEach(target => {
21
+ const widget = target.dataset.playpilotWidget || ''
22
+
23
+ const component = widgets[widget]
24
+
25
+ if (!component) return
26
+
27
+ const insertedComponent = mount(component, {
28
+ target,
29
+ props: {
30
+ linkInjections,
31
+ },
32
+ })
33
+
34
+ inTextWidgetInsertedComponents.push(insertedComponent)
35
+ })
36
+ }
37
+
38
+ export function clearInTextWidgets(): void {
39
+ inTextWidgetInsertedComponents.forEach(component => unmount(component))
40
+ document.querySelectorAll('[data-playpilot-widget]').forEach(element => element.innerHTML = '')
41
+
42
+ inTextWidgetInsertedComponents = []
43
+ }
@@ -6,6 +6,7 @@ import { clearCurrentlyHoveredInjection, destroyLinkPopover, destroyLinkPopoverO
6
6
  import { clearAfterArticlePlaylinks, insertAfterArticlePlaylinks } from './afterArticle'
7
7
  import { clearInTextDisclaimer, insertInTextDisclaimer } from './disclaimer'
8
8
  import { exploreTitleUrl, titleUrl } from './routes'
9
+ import { clearInTextWidgets, insertInTextWidgets } from './inTextWidgets'
9
10
 
10
11
  export const keyDataAttribute = 'data-playpilot-injection-key'
11
12
  export const keySelector = `[${keyDataAttribute}]`
@@ -169,6 +170,8 @@ export function injectLinksInDocument(elements: HTMLElement[], injections: LinkI
169
170
  // The function itself will decide whether or not it should actually insert the component based on the config.
170
171
  if (document.querySelector(keySelector)) insertInTextDisclaimer(elements)
171
172
 
173
+ insertInTextWidgets(foundInjections)
174
+
172
175
  return mergedInjections.filter(i => i.title_details).map((injection, index) => {
173
176
  const hasManualEquivalent = !injection.manual && isAvailableAsManualInjection(injection, index, mergedInjections)
174
177
  const duplicate = injection.duplicate ?? hasManualEquivalent
@@ -343,6 +346,7 @@ export function clearLinkInjections(): void {
343
346
 
344
347
  clearAfterArticlePlaylinks()
345
348
  clearInTextDisclaimer()
349
+ clearInTextWidgets()
346
350
  destroyAllModals(false)
347
351
  destroyLinkPopover(false)
348
352
  }
package/src/lib/routes.ts CHANGED
@@ -6,9 +6,5 @@ export function titleUrl(title: TitleData): string {
6
6
  }
7
7
 
8
8
  export function exploreTitleUrl(title: TitleData): string {
9
- if (localStorage.getItem('tpi-open-explore-as-modal') === 'true') {
10
- return window.PlayPilotLinkInjections?.config?.explore_navigation_path + `?route=modal&sid=${title.sid}`
11
- }
12
-
13
- return window.PlayPilotLinkInjections?.config?.explore_navigation_path + `?route=title&sid=${title.sid}`
9
+ return window.PlayPilotLinkInjections?.config?.explore_navigation_path + `?route=modal&sid=${title.sid}`
14
10
  }
@@ -1,4 +1,5 @@
1
1
  export type ExploreRoute = {
2
2
  key: string
3
3
  component: any
4
+ appendComponent?: any
4
5
  }
@@ -70,6 +70,8 @@
70
70
  <p>De tre ’Jurassic World’-film kunne have givet indtryk af, at der ikke skal meget til, før dinosaurer igen ville kunne dominere kloden. Men det har vist sig ikke at holde stik.</p>
71
71
  <p>Het komt elk jaar voor dat films met torenhoge budgetten flink floppen aan de box-office, ongeacht de kwaliteit van de film. Dit is het geval bij een aantal films die dit jaar zijn uitgekomen. Denk aan Mickey 17, Black Bag en de onlangs uitgebrachte animatiefilm Elio. In 2015 was de superheldenfilm Fantastic Four een van de grootste flops.</p>
72
72
 
73
+ <div data-playpilot-widget="tpi-rail"></div>
74
+
73
75
  <h2>A matching link is already present</h2>
74
76
  <p>Following their post-credits scene in <a href="/">John Wick</a>, in a new John Wick spinoff.</p>
75
77
 
@@ -226,11 +226,6 @@
226
226
 
227
227
  <hr />
228
228
 
229
- <button onclick={() => { localStorage.setItem('tpi-open-explore-as-modal', 'true'); onrerender() }}>Open links as modal in explore</button>
230
- <button onclick={() => { localStorage.setItem('tpi-open-explore-as-modal', 'false'); onrerender() }}>Open links as separate page in explore</button>
231
-
232
- <hr />
233
-
234
229
  <button onclick={() => shown = false}>Close</button>
235
230
  </div>
236
231
  {/if}
@@ -52,6 +52,11 @@
52
52
  {t('Streaming Guide Subheading')}
53
53
  </div>
54
54
 
55
+ <p class="disclaimer">
56
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -->
57
+ {@html t('Streaming Guide Disclaimer')}
58
+ </p>
59
+
55
60
  {#if !useExploreRouter()}
56
61
  <p class="description">
57
62
  {t('Streaming Guide Description')}
@@ -117,7 +122,7 @@
117
122
  .heading,
118
123
  .subheading {
119
124
  color: theme(text-color);
120
- font-size: theme(explore-heading-size, clamp(margin(1.5), 5vw, margin(2)));
125
+ font-size: theme(explore-heading-font-size, clamp(margin(1.5), 5vw, margin(2)));
121
126
  font-weight: theme(explore-heading-font-weight, font-bold);
122
127
  text-transform: theme(explore-heading-text-transform, normal);
123
128
  line-height: theme(explore-heading-line-height, 1.5);
@@ -126,13 +131,25 @@
126
131
  .subheading {
127
132
  margin-top: margin(0.5);
128
133
  max-width: margin(15);
129
- font-size: theme(explore-subheading-size, clamp(margin(1), 2.5vw, margin(1.25)));
134
+ font-size: theme(explore-subheading-font-size, clamp(margin(1), 2.5vw, margin(1.25)));
130
135
 
131
136
  @include desktop {
132
137
  max-width: 100%;
133
138
  }
134
139
  }
135
140
 
141
+ .disclaimer {
142
+ margin: theme(explore-disclaimer-margin, 0);
143
+ font-size: theme(explore-diclaimer-font-size, font-size-small);
144
+ color: theme(explore-disclaimer-color, text-color-alt);
145
+ opacity: 0.75;
146
+
147
+ :global(a) {
148
+ color: inherit;
149
+ font-style: inherit;
150
+ }
151
+ }
152
+
136
153
  .description {
137
154
  max-width: theme(explore-header-max-width, 600px);
138
155
  margin: 0;
@@ -8,9 +8,7 @@
8
8
  import ExploreResults from './Routes/ExploreResults.svelte'
9
9
  import ExploreLayout from './ExploreLayout.svelte'
10
10
  import ExploreTitle from './Routes/ExploreTitle.svelte'
11
- import { fetchSimilarTitles, fetchTitleBySid } from '$lib/api/titles'
12
- import { openModal } from '$lib/modal'
13
- import type { TitleData } from '$lib/types/title'
11
+ import ExploreModal from './Routes/ExploreModal.svelte'
14
12
 
15
13
  const routes: ExploreRoute[] = [
16
14
  {
@@ -29,6 +27,12 @@
29
27
  key: 'title',
30
28
  component: ExploreTitle,
31
29
  })
30
+
31
+ routes.push({
32
+ key: 'modal',
33
+ component: ExploreHome,
34
+ appendComponent: ExploreModal,
35
+ })
32
36
  }
33
37
 
34
38
  const initialRouteKey = getCurrentRouteParam() || routes[0].key
@@ -38,8 +42,7 @@
38
42
  let filter: ExploreFilter = $state({})
39
43
 
40
44
  const CurrentRouteComponent = $derived(currentRoute.component)
41
-
42
- if (initialRouteKey === 'modal') openModalViaRoute()
45
+ const CurrentAppendComponent = $derived(currentRoute.appendComponent)
43
46
 
44
47
  $effect(() => {
45
48
  if (searchQuery) currentRoute = routes.find(route => route.key === 'results')!
@@ -60,33 +63,12 @@
60
63
  track(TrackingEvent.ExploreNavigate, null, { route: currentRoute.key })
61
64
  }
62
65
 
63
- function onhashchange(): void {
64
- navigate(getCurrentRouteParam(), false)
65
- }
66
-
67
66
  function getCurrentRouteParam(): string {
68
67
  return new URL(document.location.toString()).searchParams.get('route') || routes[0].key
69
68
  }
70
69
 
71
- // This is temporary while testing, please clean me up later
72
- async function openModalViaRoute(): Promise<void> {
73
- const currentUrl = new URL(document.location.toString())
74
- const sid = currentUrl.searchParams.get('sid')
75
-
76
- if (!sid) return
77
-
78
- const [title, railTitles] = (await Promise.allSettled([
79
- fetchTitleBySid(sid),
80
- fetchSimilarTitles({ sid } as unknown as TitleData)])
81
- ).map(promise => (promise.status === 'fulfilled' ? promise.value : null))
82
-
83
- openModal({
84
- type: 'titles-rail',
85
- data: [(title as TitleData), ...(railTitles as TitleData[])],
86
- props: {
87
- onclose: () => navigate('home'),
88
- },
89
- })
70
+ function onhashchange(): void {
71
+ navigate(getCurrentRouteParam(), false)
90
72
  }
91
73
  </script>
92
74
 
@@ -94,4 +76,5 @@
94
76
 
95
77
  <ExploreLayout {navigate} bind:searchQuery bind:filter>
96
78
  <CurrentRouteComponent {searchQuery} {filter} {navigate} />
79
+ <CurrentAppendComponent {navigate} />
97
80
  </ExploreLayout>
@@ -0,0 +1,38 @@
1
+ <script lang="ts">
2
+ import { fetchSimilarTitles, fetchTitleBySid } from '$lib/api/titles'
3
+ import { openModal } from '$lib/modal'
4
+ import type { TitleData } from '$lib/types/title'
5
+
6
+ interface Props {
7
+ navigate?: (key: string, pushState?: boolean) => void
8
+ }
9
+
10
+ const { navigate = () => null }: Props = $props()
11
+
12
+ openModalViaRoute()
13
+
14
+ async function openModalViaRoute(): Promise<void> {
15
+ const currentUrl = new URL(document.location.toString())
16
+ const sid = currentUrl.searchParams.get('sid')
17
+
18
+ if (!sid) return
19
+
20
+ try {
21
+ const [title, railTitles] = (await Promise.allSettled([
22
+ fetchTitleBySid(sid),
23
+ fetchSimilarTitles({ sid } as unknown as TitleData)])
24
+ ).map(promise => (promise.status === 'fulfilled' ? promise.value : null))
25
+
26
+ openModal({
27
+ type: 'titles-rail',
28
+ data: [(title as TitleData), ...(railTitles as TitleData[])],
29
+ props: {
30
+ onclose: () => navigate('home'),
31
+ pushState: false,
32
+ },
33
+ })
34
+ } catch {
35
+ navigate('home')
36
+ }
37
+ }
38
+ </script>
@@ -21,6 +21,7 @@
21
21
  blur?: boolean
22
22
  closeButtonStyle?: 'shadow' | 'flat'
23
23
  initialScrollPosition?: number
24
+ pushState?: boolean
24
25
  onscroll?: () => void
25
26
  onclose?: () => void
26
27
  }
@@ -35,6 +36,7 @@
35
36
  blur = false,
36
37
  closeButtonStyle = 'shadow',
37
38
  initialScrollPosition = 0,
39
+ pushState = true,
38
40
  onscroll = () => null,
39
41
  onclose = () => null,
40
42
  }: Props = $props()
@@ -63,7 +65,7 @@
63
65
 
64
66
  // Add modal state to the browser history. This allows us to close to modal when using the back button.
65
67
  // Only do this for the very first modal opened in the stack. The back button always fully closes all modals.
66
- if (!hasPreviousModal) window.history.pushState({ modal: true }, '', historyHash)
68
+ if (pushState && !hasPreviousModal) window.history.pushState({ modal: true }, '', historyHash)
67
69
 
68
70
  requestAnimationFrame(setInitialScrollPosition)
69
71
 
@@ -16,7 +16,7 @@
16
16
  each: Snippet<[item: any, currentIndex: number]>
17
17
  }
18
18
 
19
- const { items, initialIndex = 0, onchange = () => null, onclose = () => null, each }: Props = $props()
19
+ const { items, initialIndex = 0, onchange = () => null, onclose = () => null, each, ...restProps }: Props = $props()
20
20
 
21
21
  const transitionDuration = 300
22
22
 
@@ -36,7 +36,7 @@
36
36
  }
37
37
  </script>
38
38
 
39
- <Modal blur {onclose}>
39
+ <Modal blur {onclose} {...restProps}>
40
40
  {#snippet dialog()}
41
41
  <div class="rail-modal" style:--transition-duration="{transitionDuration}ms">
42
42
  <TinySlider threshold={40} moveThreshold={40} transitionDuration={initialized ? transitionDuration : 0} bind:this={slider}>
@@ -6,14 +6,15 @@
6
6
  import RailModal from './RailModal.svelte'
7
7
 
8
8
  interface Props {
9
- titles: TitleData[]
10
- initialIndex?: number
9
+ titles: TitleData[],
10
+ initialIndex?: number,
11
+ onclose?: () => void
11
12
  }
12
13
 
13
- const { titles, initialIndex = 0 }: Props = $props()
14
+ const { titles, initialIndex = 0, onclose = () => null, ...restProps }: Props = $props()
14
15
  </script>
15
16
 
16
- <RailModal items={titles} {initialIndex} onchange={(index) => track(TrackingEvent.ExploreTitleRailSetIndex, titles[index], { index })}>
17
+ <RailModal items={titles} {initialIndex} {onclose} onchange={(index) => track(TrackingEvent.ExploreTitleRailSetIndex, titles[index], { index })} {...restProps}>
17
18
  {#snippet each(title, currentIndex)}
18
19
  <Title {title} useVideoBackground={title.sid === titles[currentIndex]?.sid} />
19
20
  {/snippet}
@@ -58,15 +58,15 @@
58
58
  .rail {
59
59
  --gap: var(--rail-gap, #{margin(0.5)});
60
60
  position: relative;
61
- width: calc(100% + margin(2));
62
- margin: 0 margin(-1);
61
+ width: calc(100% + var(--rail-margin, margin(1)) * 2);
62
+ margin: 0 calc(var(--rail-margin, margin(1)) * -1);
63
63
 
64
64
  :global(.slider) {
65
- padding: 0 margin(1);
65
+ padding: 0 var(--rail-margin, margin(1));
66
66
  }
67
67
 
68
68
  :global(.slider-content > :last-child) {
69
- margin-right: margin(2);
69
+ margin-right: calc(var(--rail-margin, margin(1)) * 2);
70
70
  }
71
71
  }
72
72
 
@@ -0,0 +1,51 @@
1
+ <script lang="ts">
2
+ import { TrackingEvent } from '$lib/enums/TrackingEvent'
3
+ import { t } from '$lib/localization'
4
+ import { track } from '$lib/tracking'
5
+ import type { LinkInjection } from '$lib/types/injection'
6
+ import type { TitleData } from '$lib/types/title'
7
+ import TitlesRail from '../Rails/TitlesRail.svelte'
8
+
9
+ interface Props {
10
+ linkInjections: LinkInjection[]
11
+ }
12
+
13
+ const { linkInjections }: Props = $props()
14
+
15
+ let element: HTMLElement | null = $state(null)
16
+
17
+ // @ts-ignore
18
+ const heading = $derived(element?.parentNode?.dataset.heading || t('Mentioned In This Article'))
19
+
20
+ const titles: TitleData[] = $derived.by(() => {
21
+ const uniqueTitles: TitleData[] = []
22
+
23
+ linkInjections.forEach(injection => {
24
+ const title = injection.title_details!
25
+
26
+ if (!title) return
27
+ if (uniqueTitles.some(t => t.sid === title.sid)) return
28
+
29
+ uniqueTitles.push(title)
30
+ })
31
+
32
+ return uniqueTitles
33
+ })
34
+ </script>
35
+
36
+ {#if titles.length}
37
+ <div class="widget" bind:this={element} data-testid="widget">
38
+ <TitlesRail {titles} {heading} onclick={(title) => track(TrackingEvent.WidgetArticleRailTitleClick, title)} />
39
+ </div>
40
+ {/if}
41
+
42
+ <style lang="scss">
43
+ .widget {
44
+ --rail-margin: 0;
45
+ --playpilot-rails-arrow-background: black;
46
+ --playpilot-detail-background-light: black;
47
+ --playpilot-rail-title-text-color: currentColor;
48
+ --playpilot-rail-text-color: currentColor;
49
+ margin: margin(1) 0;
50
+ }
51
+ </style>
@@ -29,6 +29,10 @@
29
29
  setTimeout(insertExplore, 100)
30
30
  </script>
31
31
 
32
+ <svelte:head>
33
+ <title>PlayPilot Link Injections - Explore</title>
34
+ </svelte:head>
35
+
32
36
  <button onclick={(() => {
33
37
  destroyExplore()
34
38
  window.PlayPilotLinkInjections.config.explore_use_router = !useExploreRouter()