Skip to content

Commit 84105ef

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

7 files changed

Lines changed: 184 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: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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.map((script) => {
24+
const scriptProps: ComponentProps<typeof Script> = {
25+
strategy,
26+
};
27+
28+
// Handle external scripts (SrcScript)
29+
if (script.__typename === 'SrcScript' && script.src) {
30+
scriptProps.src = script.src;
31+
32+
// Add integrity hashes if provided
33+
if (script.integrityHashes.length > 0) {
34+
scriptProps.integrity = script.integrityHashes.map((hashObj) => hashObj.hash).join(' ');
35+
scriptProps.crossOrigin = 'anonymous';
36+
}
37+
}
38+
39+
// Handle inline scripts (InlineScript)
40+
if (script.__typename === 'InlineScript' && script.scriptTag) {
41+
const scriptMatch = /<script[^>]*>([\s\S]*?)<\/script>/i.exec(script.scriptTag);
42+
43+
if (scriptMatch?.[1]) {
44+
scriptProps.dangerouslySetInnerHTML = {
45+
__html: scriptMatch[1],
46+
};
47+
}
48+
}
49+
50+
scriptProps.id = `bc-script-${script.entityId}`;
51+
52+
if (!scriptProps.src && !scriptProps.dangerouslySetInnerHTML) {
53+
return null;
54+
}
55+
return <Script key={scriptProps.id} {...scriptProps} />;
56+
})}
57+
</>
58+
);
59+
};

0 commit comments

Comments
 (0)