-
Notifications
You must be signed in to change notification settings - Fork 287
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
24 changed files
with
6,093 additions
and
2,064 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
GTM_CONTAINER_ID=GTM-1234567 | ||
|
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,40 +1,225 @@ | ||
# Hydrogen template: Skeleton | ||
# Hydrogen example: Partytown + Google Tag Manager + CSP | ||
|
||
Hydrogen is Shopify’s stack for headless commerce. Hydrogen is designed to dovetail with [Remix](https://remix.run/), Shopify’s full stack web framework. This template contains a **minimal setup** of components, queries and tooling to get started with Hydrogen. | ||
This folder contains a peformance-oriented example lazy-loading [Google Tag Manager](https://support.google.com/tagmanager) | ||
using [Partytown](https://partytown.builder.io/). | ||
|
||
[Check out Hydrogen docs](https://shopify.dev/custom-storefronts/hydrogen) | ||
[Get familiar with Remix](https://remix.run/docs/en/v1) | ||
Party town helps relocate resource intensive scripts into a web worker, and off of the main thread. | ||
Its goal is to help speed up sites by dedicating the main thread to your code, and offloading third-party scripts to a web worker. | ||
|
||
## What's included | ||
## Requirements | ||
|
||
- Remix | ||
- Hydrogen | ||
- Oxygen | ||
- Shopify CLI | ||
- ESLint | ||
- Prettier | ||
- GraphQL generator | ||
- TypeScript and JavaScript flavors | ||
- Minimal setup of components and routes | ||
- [Google Tag Manager ID] - Log in to your Google Tag Manager account and open a container. In the top right corner (next to the Submit and Preview buttons) you'll see some short text that starts with GTM- and then contains some letters/numbers. That's your Google Tag Manager ID | ||
- [Basic Partytown knowledge](https://dev.to/adamdbradley/introducing-partytown-run-third-party-scripts-from-a-web-worker-2cnp) - Introducing Partytown: Run Third-Party Scripts From a Web Worker | ||
|
||
## Getting started | ||
## Key files | ||
|
||
**Requirements:** | ||
This folder contains the minimal set of files needed to showcase the implementation. | ||
Files that aren’t included by default with Hydrogen and that you’ll need to | ||
create are labeled with 🆕. | ||
|
||
- Node.js version 16.14.0 or higher | ||
| File | Description | | ||
| ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| 🆕 [`.env.example`](.env.example) | Example environment variable file. Copy the relevant variables to your existing `.env` file, if you have one. | | ||
| 🆕 [`app/components/PartytownGoogleTagManager.tsx`](app/components/PartytownGoogleTagManager.tsx) | A component that loads Google Tag Manager in a web worker via Partytown with built-in CSP support. | | ||
| 🆕 [`app/utils/partytown/maybeProxyRequest.ts`](app/utils/partytown/maybeProxyRequest.ts) | A Partytown url resolver to control which 3P scripts should be reverse-proxied. Used in Partytown's [resolveUrl](https://partytown.builder.io/proxying-requests#configuring-url-proxies) property | | ||
| 🆕 [`app/utils/partytown/partytownAtomicHeaders.ts`](app/lib/partytown/partytownAtomicHeaders.ts) | Utility that returns the required headers to enable [Atomics mode](https://partytown.builder.io/atomics) for added performance | | ||
| 🆕 [`app/routes/reverse-proxy.ts`](app/routes/reverse-proxy.ts) | A route that acts as a [reverse proxy](https://partytown.builder.io/proxying-requests#reverse-proxy) for 3P scripts that require CORS headers | | ||
| [`app/root.tsx`](app/root.tsx) | The root layout where Partytown and GTM is implemented | | ||
| [`app/routes/_index.tsx`](app/routes/_index.tsx) | The home route where a GTM `pageView` event is emmited | | ||
| [`app/entry.server.tsx`](app/entry.server.tsx) | Add GTM domain to the script-src directive | | ||
|
||
## Dependencies | ||
|
||
| Module | Description | | ||
| ------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| 🆕 [@builder.io/partytown](https://www.npmjs.com/package/@builder.io/partytown) | Partytown is a lazy-loaded library to help relocate resource intensive scripts into a web worker, and off of the main thread. Its goal is to help speed up sites by dedicating the main thread to your code, and offloading third-party scripts to a web worker. | | ||
|
||
## Instructions | ||
|
||
### 1. Install required dependencies | ||
|
||
```bash | ||
npm create @shopify/hydrogen@latest | ||
npm i @builder.io/partytown | ||
``` | ||
|
||
## Building for production | ||
### 2. Modify npm scripts | ||
|
||
```bash | ||
npm run build | ||
In `package.json` modify the `build` script and add the `partytown` script | ||
|
||
Add the `partytown` script which copies the [library files](https://partytown.builder.io/copy-library-files) to `/public` | ||
|
||
```diff | ||
"scripts": { | ||
+ "partytown": "partytown copylib public/~partytown" | ||
}, | ||
``` | ||
|
||
## Local development | ||
Modify the `build` script to copy the partytown library files to `/public` before every build | ||
|
||
```diff | ||
"scripts": { | ||
- "build": "shopify hydrogen build", | ||
+ "build": "npm run partytown && shopify hydrogen build", | ||
}, | ||
``` | ||
|
||
[View the complete component file](package.json) to see these updates in context. | ||
|
||
### 3. Copy the library files | ||
|
||
```bash | ||
npm run dev | ||
npm run partytown | ||
``` | ||
|
||
### 4. Copy over the new files | ||
|
||
- In your Hydrogen app, create the new files from the file list above, copying in the code as you go. | ||
- If you already have a `.env` file, copy over these key-value pairs: | ||
- `GTM_CONTAINER_ID` - To obtain your GTM container ID follow these [instructions](https://support.google.com/tagmanager/answer/6103696?hl=en&ref_topic=3441530&sjid=7981978906794913873-NC) | ||
|
||
### 5. Edit the `root.tsx` layout file | ||
|
||
Import the required components and utilties | ||
|
||
```ts | ||
import {Partytown} from '@builder.io/partytown/react'; | ||
import {PartytownGoogleTagManager} from '~/components/PartytownGoogleTagManager'; | ||
import {maybeProxyRequest} from '~/utils/partytown/maybeProxyRequest'; | ||
import {partytownAtomicHeaders} from '~/utils/partytown/partytownAtomicHeaders'; | ||
``` | ||
|
||
Update the `loader` function | ||
|
||
```ts | ||
export async function loader({context}: LoaderFunctionArgs) { | ||
const layout = await context.storefront.query<{shop: Shop}>(LAYOUT_QUERY); | ||
return json( | ||
{ | ||
layout, | ||
// 1. Pass the GTM container ID to the client | ||
gtmContainerId: context.env.GTM_CONTAINER_ID, | ||
}, | ||
{ | ||
// 2. Enable atomic mode | ||
headers: partytownAtomicHeaders(), | ||
}, | ||
); | ||
} | ||
``` | ||
|
||
Update the App component | ||
|
||
```ts | ||
export default function App() { | ||
// 1. Retrieve the GTM container ID | ||
const {gtmContainerId} = useLoaderData<typeof loader>(); | ||
const nonce = useNonce(); | ||
|
||
return ( | ||
<html lang="en"> | ||
<head> | ||
<Meta /> | ||
<Links /> | ||
</head> | ||
|
||
<body> | ||
<Outlet /> | ||
<ScrollRestoration nonce={nonce} /> | ||
<Scripts nonce={nonce} /> | ||
|
||
{/* 2. Initialize the GTM dataLayer container */} | ||
<Script | ||
type="text/partytown" | ||
dangerouslySetInnerHTML={{ | ||
__html: ` | ||
dataLayer = window.dataLayer || []; | ||
window.gtag = function () { | ||
dataLayer.push(arguments); | ||
}; | ||
window.gtag('js', new Date()); | ||
window.gtag('config', "${gtmContainerId}"); | ||
`, | ||
}} | ||
/> | ||
|
||
{/* 3. Include the GTM component */} | ||
<PartytownGoogleTagManager gtmContainerId={gtmContainerId} /> | ||
|
||
{/* 4. Initialize PartyTown */} | ||
<Partytown | ||
nonce={nonce} | ||
forward={['dataLayer.push', 'gtag']} | ||
resolveUrl={maybeProxyRequest} | ||
/> | ||
</body> | ||
</html> | ||
); | ||
} | ||
``` | ||
|
||
[View the complete component file](app/root.tsx) to see these updates in context. | ||
|
||
## 6. (Optional) - Update Content Securirt Policy | ||
|
||
Add `wwww.googletagmanager.com` domain to the `scriptSrc` directive | ||
|
||
```diff | ||
//...other code | ||
|
||
export default async function handleRequest( | ||
request: Request, | ||
responseStatusCode: number, | ||
responseHeaders: Headers, | ||
remixContext: EntryContext, | ||
) { | ||
- const {nonce, header, NonceProvider} = createContentSecurityPolicy(); | ||
+ const {nonce, header, NonceProvider} = createContentSecurityPolicy({ | ||
+ scriptSrc: ["'self'", 'cdn.shopify.com', 'www.googletagmanager.com'], | ||
+ }); | ||
|
||
//...other code | ||
|
||
responseHeaders.set('Content-Security-Policy', header); | ||
|
||
return new Response(body, { | ||
headers: responseHeaders, | ||
status: responseStatusCode, | ||
}); | ||
} | ||
|
||
``` | ||
[View the complete component file](app/entry.server.tsx) to see these updates in context. | ||
## 7. (TypeScript only) - Add the new environment variable to the `ENV` type definition | ||
Update the `remix.d.ts` file | ||
```diff | ||
// ...other code | ||
|
||
declare global { | ||
/** | ||
* A global `process` object is only available during build to access NODE_ENV. | ||
*/ | ||
const process: {env: {NODE_ENV: 'production' | 'development'}}; | ||
|
||
/** | ||
* Declare expected Env parameter in fetch handler. | ||
*/ | ||
interface Env { | ||
SESSION_SECRET: string; | ||
PUBLIC_STOREFRONT_API_TOKEN: string; | ||
PRIVATE_STOREFRONT_API_TOKEN: string; | ||
PUBLIC_STORE_DOMAIN: string; | ||
PUBLIC_STOREFRONT_ID: string; | ||
+ GTM_CONTAINER_ID: `GTM-${string}`; | ||
} | ||
} | ||
|
||
// ...other code | ||
``` | ||
[View the complete component file](remix.d.ts) to see these updates in context. |
68 changes: 68 additions & 0 deletions
68
examples/partytown/app/components/PartytownGoogleTagManager.tsx
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,68 @@ | ||
import {useEffect, useRef} from 'react'; | ||
|
||
/** | ||
* Component to add Google Tag Manager via Partytown | ||
* @see https://partytown.builder.io/google-tag-manager | ||
*/ | ||
export function PartytownGoogleTagManager(props: { | ||
gtmContainerId: string | undefined; | ||
dataLayerKey?: string; | ||
}) { | ||
const init = useRef(false); | ||
const {gtmContainerId, dataLayerKey = 'dataLayer'} = props; | ||
|
||
useEffect(() => { | ||
if (init.current || !gtmContainerId) { | ||
return; | ||
} | ||
|
||
const gtmScript = document.createElement('script'); | ||
const nonceScript = document.querySelector('[nonce]') as | ||
| HTMLScriptElement | ||
| undefined; | ||
if (nonceScript?.nonce) { | ||
gtmScript.setAttribute('nonce', nonceScript.nonce); | ||
} | ||
|
||
gtmScript.innerHTML = ` | ||
(function(w, d, s, l, i) { | ||
w[l] = w[l] || []; | ||
w[l].push({ | ||
'gtm.start': new Date().getTime(), | ||
event: 'gtm.js' | ||
}); | ||
var f = d.getElementsByTagName(s)[0], | ||
j = d.createElement(s), | ||
dl = l != 'dataLayer' ? '&l=' + l : ''; | ||
j.type = "text/partytown" | ||
j.src = | ||
'https://www.googletagmanager.com/gtm.js?id=' + i + dl + '&version=' + Date.now(); | ||
f.parentNode.insertBefore(j, f); | ||
})(window, document, 'script', '${dataLayerKey}', '${gtmContainerId}'); | ||
`; | ||
|
||
// Add the partytown GTM script to the body | ||
document.body.appendChild(gtmScript); | ||
|
||
init.current = true; | ||
return () => { | ||
document.body.removeChild(gtmScript); | ||
}; | ||
}, [dataLayerKey, gtmContainerId]); | ||
|
||
if (!gtmContainerId) { | ||
return null; | ||
} | ||
|
||
return ( | ||
<noscript> | ||
{/* GOOGLE TAG MANAGER NO-JS FALLBACK */} | ||
<iframe | ||
src={`https://www.googletagmanager.com/ns.html?id=${gtmContainerId}`} | ||
height="0" | ||
width="0" | ||
style={{display: 'none', visibility: 'hidden'}} | ||
/> | ||
</noscript> | ||
); | ||
} |
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.