Skip to content

Commit 292574f

Browse files
author
Nathan Booker
committed
feat: render scripts from Script Manager
1 parent 6de33ab commit 292574f

7 files changed

Lines changed: 193 additions & 1 deletion

File tree

.changeset/wise-ads-drive.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@bigcommerce/catalyst-core": minor
3+
---
4+
5+
Add support for Scripts API/Script Manager scripts rendering via next/script

core/app/[locale]/layout.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { revalidate } from '~/client/revalidate-target';
1919
import { WebAnalyticsFragment } from '~/components/analytics/fragment';
2020
import { AnalyticsProvider } from '~/components/analytics/provider';
2121
import { ContainerQueryPolyfill } from '~/components/polyfills/container-query';
22+
import { ScriptManagerScripts, ScriptsFragment } from '~/components/scripts';
2223
import { routing } from '~/i18n/routing';
2324
import { getToastNotification } from '~/lib/server-toast';
2425

@@ -35,13 +36,16 @@ const RootLayoutMetadataQuery = graphql(
3536
}
3637
...WebAnalyticsFragment
3738
}
39+
content {
40+
...ScriptsFragment
41+
}
3842
}
3943
channel {
4044
entityId
4145
}
4246
}
4347
`,
44-
[WebAnalyticsFragment],
48+
[WebAnalyticsFragment, ScriptsFragment],
4549
);
4650

4751
const fetchRootLayoutMetadata = cache(async () => {
@@ -109,6 +113,12 @@ export default async function RootLayout({ params, children }: Props) {
109113

110114
return (
111115
<html className={clsx(fonts.map((f) => f.variable))} lang={locale}>
116+
<head>
117+
<ScriptManagerScripts
118+
scripts={data.site.content.headerScripts}
119+
strategy="afterInteractive"
120+
/>
121+
</head>
112122
<body className="flex min-h-screen flex-col">
113123
<NextIntlClientProvider>
114124
<NuqsAdapter>
@@ -124,6 +134,7 @@ export default async function RootLayout({ params, children }: Props) {
124134
</NextIntlClientProvider>
125135
<VercelComponents />
126136
<ContainerQueryPolyfill />
137+
<ScriptManagerScripts scripts={data.site.content.footerScripts} strategy="lazyOnload" />
127138
</body>
128139
</html>
129140
);

core/client/graphql.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const graphql = initGraphQLTada<{
88
DateTime: string;
99
Long: number;
1010
BigDecimal: number;
11+
UUID: string;
1112
};
1213
disableMasking: true;
1314
}>();

core/components/scripts/README.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Script Components
2+
3+
This directory contains components for rendering BigCommerce scripts using Next.js Script component.
4+
5+
## Components
6+
7+
### ScriptManagerScripts
8+
A single component that renders scripts with configurable strategy. Location filtering is now handled at the GraphQL level, simplifying the component logic.
9+
10+
## Usage
11+
12+
```tsx
13+
import { ScriptManagerScripts } from '~/components/scripts';
14+
15+
// In your layout component:
16+
<head>
17+
<ScriptManagerScripts
18+
scripts={data.site.content.headerScripts}
19+
strategy="afterInteractive"
20+
/>
21+
</head>
22+
<body>
23+
{children}
24+
<ScriptManagerScripts
25+
scripts={data.site.content.footerScripts}
26+
strategy="lazyOnload"
27+
/>
28+
</body>
29+
```
30+
31+
## Architecture
32+
33+
The component accepts:
34+
- `scripts`: Pre-filtered GraphQL scripts data (headerScripts or footerScripts)
35+
- `strategy`: 'afterInteractive', 'lazyOnload', or other Next.js Script strategies
36+
37+
## Script Processing
38+
39+
The component handles both types of BigCommerce scripts:
40+
41+
### External Scripts (SrcScript)
42+
```tsx
43+
<Script
44+
id="bc-script-123"
45+
src="https://example.com/script.js"
46+
strategy="afterInteractive"
47+
integrity="sha256-abc123..."
48+
crossOrigin="anonymous"
49+
/>
50+
```
51+
52+
### Inline Scripts (InlineScript)
53+
Inline scripts are rendered using `dangerouslySetInnerHTML` for reliable execution:
54+
```tsx
55+
<Script
56+
id="bc-script-456"
57+
strategy="lazyOnload"
58+
dangerouslySetInnerHTML={{ __html: scriptContent }}
59+
/>
60+
```
61+
62+
## Performance & Security
63+
64+
- Header scripts use `afterInteractive` strategy for critical functionality
65+
- Footer scripts use `lazyOnload` strategy for better performance
66+
- Integrity hashes are included when available for security
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { graphql } from '~/client/graphql';
2+
3+
export const ScriptsFragment = graphql(`
4+
fragment ScriptsFragment on Content {
5+
headerScripts: scripts(
6+
first: 50
7+
filters: { visibilities: [ALL_PAGES, STOREFRONT], location: HEAD }
8+
) {
9+
...ScriptTypeConnectionFragment
10+
}
11+
footerScripts: scripts(
12+
first: 50
13+
filters: { visibilities: [ALL_PAGES, STOREFRONT], location: FOOTER }
14+
) {
15+
...ScriptTypeConnectionFragment
16+
}
17+
}
18+
19+
fragment ScriptTypeConnectionFragment on ScriptTypeConnection {
20+
edges {
21+
node {
22+
__typename
23+
integrityHashes {
24+
hash
25+
}
26+
entityId
27+
consentCategory
28+
location
29+
visibility
30+
... on InlineScript {
31+
scriptTag
32+
}
33+
... on SrcScript {
34+
src
35+
}
36+
}
37+
}
38+
}
39+
`);

core/components/scripts/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { ScriptManagerScripts } from './scripts';
2+
export { ScriptsFragment } from './fragment';
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client';
2+
import Script from 'next/script';
3+
import { ComponentProps } from 'react';
4+
5+
import { FragmentOf } from '~/client/graphql';
6+
7+
import { ScriptsFragment } from './fragment';
8+
9+
type ScriptsData = FragmentOf<typeof ScriptsFragment>;
10+
11+
interface ScriptRendererProps {
12+
scripts: ScriptsData['headerScripts'] | null;
13+
strategy: ComponentProps<typeof Script>['strategy'];
14+
}
15+
16+
export const ScriptManagerScripts = ({ scripts, strategy }: ScriptRendererProps) => {
17+
if (!scripts?.edges) return null;
18+
19+
const scriptNodes = removeEdgesAndNodes(scripts);
20+
21+
return (
22+
<>
23+
{scriptNodes
24+
.map((script) => {
25+
const scriptProps: ComponentProps<typeof Script> = {
26+
strategy,
27+
};
28+
29+
// Handle external scripts (SrcScript)
30+
if (script.__typename === 'SrcScript' && script.src) {
31+
scriptProps.src = script.src;
32+
33+
// Add integrity hashes if provided
34+
if (script.integrityHashes.length > 0) {
35+
scriptProps.integrity = script.integrityHashes
36+
.map((hashObj) => hashObj.hash)
37+
.join(' ');
38+
scriptProps.crossOrigin = 'anonymous';
39+
}
40+
}
41+
42+
// Handle inline scripts (InlineScript)
43+
if (script.__typename === 'InlineScript' && script.scriptTag) {
44+
const scriptMatch = /<script[^>]*>([\s\S]*?)<\/script>/i.exec(script.scriptTag);
45+
46+
if (scriptMatch?.[1]) {
47+
scriptProps.dangerouslySetInnerHTML = {
48+
__html: scriptMatch[1],
49+
};
50+
}
51+
}
52+
53+
scriptProps.id = `bc-script-${script.entityId}`;
54+
55+
// Return null for invalid scripts (will be filtered out)
56+
if (!scriptProps.src && !scriptProps.dangerouslySetInnerHTML) {
57+
return null;
58+
}
59+
60+
return scriptProps;
61+
})
62+
.filter((scriptProps): scriptProps is ComponentProps<typeof Script> => scriptProps !== null)
63+
.map((scriptProps) => {
64+
return <Script key={scriptProps.id} {...scriptProps} />;
65+
})}
66+
</>
67+
);
68+
};

0 commit comments

Comments
 (0)