✨ Add Google Tag Manager (#185)
This commit is contained in:
@@ -36,6 +36,8 @@ export const MetadataForm = ({
|
|||||||
onMetadataChange({ ...metadata, favIconUrl })
|
onMetadataChange({ ...metadata, favIconUrl })
|
||||||
const handleImageSubmit = (imageUrl: string) =>
|
const handleImageSubmit = (imageUrl: string) =>
|
||||||
onMetadataChange({ ...metadata, imageUrl })
|
onMetadataChange({ ...metadata, imageUrl })
|
||||||
|
const handleGoogleTagManagerIdChange = (googleTagManagerId: string) =>
|
||||||
|
onMetadataChange({ ...metadata, googleTagManagerId })
|
||||||
const handleHeadCodeChange = (customHeadCode: string) =>
|
const handleHeadCodeChange = (customHeadCode: string) =>
|
||||||
onMetadataChange({ ...metadata, customHeadCode })
|
onMetadataChange({ ...metadata, customHeadCode })
|
||||||
|
|
||||||
@@ -92,26 +94,23 @@ export const MetadataForm = ({
|
|||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack>
|
|
||||||
<FormLabel mb="0" htmlFor="title">
|
|
||||||
Title:
|
|
||||||
</FormLabel>
|
|
||||||
<Input
|
<Input
|
||||||
id="title"
|
label="Title:"
|
||||||
defaultValue={metadata.title ?? typebotName}
|
defaultValue={metadata.title ?? typebotName}
|
||||||
onChange={handleTitleChange}
|
onChange={handleTitleChange}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
|
||||||
<Stack>
|
|
||||||
<FormLabel mb="0" htmlFor="description">
|
|
||||||
Description:
|
|
||||||
</FormLabel>
|
|
||||||
<Textarea
|
<Textarea
|
||||||
id="description"
|
|
||||||
defaultValue={metadata.description}
|
defaultValue={metadata.description}
|
||||||
onChange={handleDescriptionChange}
|
onChange={handleDescriptionChange}
|
||||||
|
label="Description:"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
defaultValue={metadata.googleTagManagerId}
|
||||||
|
placeholder="GTM-XXXXXX"
|
||||||
|
onChange={handleGoogleTagManagerIdChange}
|
||||||
|
label="Google Tag Manager ID:"
|
||||||
|
moreInfoTooltip="Do not include it if you are embedding your typebot in an existing website. GTM should be installed in the parent website instead."
|
||||||
/>
|
/>
|
||||||
</Stack>
|
|
||||||
<Stack>
|
<Stack>
|
||||||
<HStack as={FormLabel} mb="0" htmlFor="head">
|
<HStack as={FormLabel} mb="0" htmlFor="head">
|
||||||
<Text>Custom head code:</Text>
|
<Text>Custom head code:</Text>
|
||||||
|
|||||||
@@ -101,13 +101,10 @@ test.describe.parallel('Settings page', () => {
|
|||||||
await page.fill('input[placeholder="Paste the image link..."]', imageUrl)
|
await page.fill('input[placeholder="Paste the image link..."]', imageUrl)
|
||||||
await expect(websiteImg).toHaveAttribute('src', imageUrl)
|
await expect(websiteImg).toHaveAttribute('src', imageUrl)
|
||||||
|
|
||||||
// Title
|
await page.getByRole('textbox', { name: 'Title' }).fill('Awesome typebot')
|
||||||
await page.fill('input#title', 'Awesome typebot')
|
await page
|
||||||
|
.getByRole('textbox', { name: 'Description' })
|
||||||
// Description
|
.fill('Lorem ipsum')
|
||||||
await page.fill('textarea#description', 'Lorem ipsum')
|
|
||||||
|
|
||||||
// Custom head code
|
|
||||||
await page.fill(
|
await page.fill(
|
||||||
'div[contenteditable=true]',
|
'div[contenteditable=true]',
|
||||||
'<script>Lorem ipsum</script>'
|
'<script>Lorem ipsum</script>'
|
||||||
|
|||||||
@@ -38,3 +38,9 @@ You can tweak `3000` (3s) to your liking.
|
|||||||
In the Metadata section, you can customize how the preview card will look if you share your bot URL on social media for example.
|
In the Metadata section, you can customize how the preview card will look if you share your bot URL on social media for example.
|
||||||
|
|
||||||
You can also add some custom head code to add third-party scripts like a Facebook pixel for example.
|
You can also add some custom head code to add third-party scripts like a Facebook pixel for example.
|
||||||
|
|
||||||
|
### Google Tag Manager
|
||||||
|
|
||||||
|
Allows you to easily add a GTM container to your bot. To find your GTM container ID, go to your GTM dashboard and click on the container you want to use. The ID is displayed in the top right corner.
|
||||||
|
|
||||||
|
Note that you should not include it if you are embedding your typebot in an existing website. GTM should be installed in the parent website instead.
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
import { gtmHeadSnippet } from '@/lib/google-tag-manager'
|
||||||
import { Metadata } from 'models'
|
import { Metadata } from 'models'
|
||||||
import Head from 'next/head'
|
import Head from 'next/head'
|
||||||
|
import Script from 'next/script'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { isNotEmpty } from 'utils'
|
||||||
|
|
||||||
type SEOProps = {
|
type SEOProps = {
|
||||||
url: string
|
url: string
|
||||||
@@ -11,9 +14,10 @@ type SEOProps = {
|
|||||||
export const SEO = ({
|
export const SEO = ({
|
||||||
url,
|
url,
|
||||||
typebotName,
|
typebotName,
|
||||||
metadata: { title, description, favIconUrl, imageUrl },
|
metadata: { title, description, favIconUrl, imageUrl, googleTagManagerId },
|
||||||
}: SEOProps) => (
|
}: SEOProps) => (
|
||||||
<Head>
|
<>
|
||||||
|
<Head key="seo">
|
||||||
<title>{title ?? typebotName}</title>
|
<title>{title ?? typebotName}</title>
|
||||||
<meta name="robots" content="noindex" />
|
<meta name="robots" content="noindex" />
|
||||||
<link
|
<link
|
||||||
@@ -62,4 +66,10 @@ export const SEO = ({
|
|||||||
content={imageUrl ?? 'https://bot.typebot.io/site-preview.png'}
|
content={imageUrl ?? 'https://bot.typebot.io/site-preview.png'}
|
||||||
/>
|
/>
|
||||||
</Head>
|
</Head>
|
||||||
|
{isNotEmpty(googleTagManagerId) && (
|
||||||
|
<Script id="google-tag-manager">
|
||||||
|
{gtmHeadSnippet(googleTagManagerId)}
|
||||||
|
</Script>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import { TypebotViewer } from 'bot-engine'
|
|||||||
import { AnswerInput, PublicTypebot, Typebot, VariableWithValue } from 'models'
|
import { AnswerInput, PublicTypebot, Typebot, VariableWithValue } from 'models'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { isDefined, isNotDefined } from 'utils'
|
import { isDefined, isNotDefined, isNotEmpty } from 'utils'
|
||||||
import { SEO } from './Seo'
|
import { SEO } from './Seo'
|
||||||
import { ErrorPage } from './ErrorPage'
|
import { ErrorPage } from './ErrorPage'
|
||||||
import { createResultQuery, updateResultQuery } from '@/features/results'
|
import { createResultQuery, updateResultQuery } from '@/features/results'
|
||||||
import { upsertAnswerQuery } from '@/features/answers'
|
import { upsertAnswerQuery } from '@/features/answers'
|
||||||
|
import { gtmBodyElement } from '@/lib/google-tag-manager'
|
||||||
|
|
||||||
export type TypebotPageProps = {
|
export type TypebotPageProps = {
|
||||||
publishedTypebot: Omit<PublicTypebot, 'createdAt' | 'updatedAt'> & {
|
publishedTypebot: Omit<PublicTypebot, 'createdAt' | 'updatedAt'> & {
|
||||||
@@ -51,6 +52,8 @@ export const TypebotPage = ({
|
|||||||
initializeResult().then()
|
initializeResult().then()
|
||||||
if (isDefined(customHeadCode))
|
if (isDefined(customHeadCode))
|
||||||
document.head.innerHTML = document.head.innerHTML + customHeadCode
|
document.head.innerHTML = document.head.innerHTML + customHeadCode
|
||||||
|
const gtmId = publishedTypebot.settings.metadata.googleTagManagerId
|
||||||
|
if (isNotEmpty(gtmId)) document.body.prepend(gtmBodyElement(gtmId))
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|||||||
@@ -125,12 +125,14 @@ test('Show close message', async ({ page }) => {
|
|||||||
|
|
||||||
test('Should correctly parse metadata', async ({ page }) => {
|
test('Should correctly parse metadata', async ({ page }) => {
|
||||||
const typebotId = cuid()
|
const typebotId = cuid()
|
||||||
|
const googleTagManagerId = 'GTM-M72NXKB'
|
||||||
const customMetadata: Metadata = {
|
const customMetadata: Metadata = {
|
||||||
description: 'My custom description',
|
description: 'My custom description',
|
||||||
title: 'Custom title',
|
title: 'Custom title',
|
||||||
favIconUrl: 'https://www.baptistearno.com/favicon.png',
|
favIconUrl: 'https://www.baptistearno.com/favicon.png',
|
||||||
imageUrl: 'https://www.baptistearno.com/images/site-preview.png',
|
imageUrl: 'https://www.baptistearno.com/images/site-preview.png',
|
||||||
customHeadCode: '<meta name="author" content="John Doe">',
|
customHeadCode: '<meta name="author" content="John Doe">',
|
||||||
|
googleTagManagerId,
|
||||||
}
|
}
|
||||||
await createTypebots([
|
await createTypebots([
|
||||||
{
|
{
|
||||||
@@ -146,6 +148,11 @@ test('Should correctly parse metadata', async ({ page }) => {
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
await page.goto(`/${typebotId}-public`)
|
await page.goto(`/${typebotId}-public`)
|
||||||
|
await expect(
|
||||||
|
typebotViewer(page).locator(
|
||||||
|
`input[placeholder="${defaultTextInputOptions.labels.placeholder}"]`
|
||||||
|
)
|
||||||
|
).toBeVisible()
|
||||||
expect(
|
expect(
|
||||||
await page.evaluate(`document.querySelector('title').textContent`)
|
await page.evaluate(`document.querySelector('title').textContent`)
|
||||||
).toBe(customMetadata.title)
|
).toBe(customMetadata.title)
|
||||||
@@ -177,9 +184,12 @@ test('Should correctly parse metadata', async ({ page }) => {
|
|||||||
.content
|
.content
|
||||||
)
|
)
|
||||||
).toBe('John Doe')
|
).toBe('John Doe')
|
||||||
await expect(
|
expect(
|
||||||
typebotViewer(page).locator(
|
await page.evaluate(
|
||||||
`input[placeholder="${defaultTextInputOptions.labels.placeholder}"]`
|
(googleTagManagerId) =>
|
||||||
|
document.querySelector(
|
||||||
|
`iframe[src="https://www.googletagmanager.com/ns.html?id=${googleTagManagerId}"]`
|
||||||
|
) as HTMLMetaElement
|
||||||
)
|
)
|
||||||
).toBeVisible()
|
).toBeDefined()
|
||||||
})
|
})
|
||||||
|
|||||||
23
apps/viewer/src/lib/google-tag-manager.ts
Normal file
23
apps/viewer/src/lib/google-tag-manager.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
export const gtmHeadSnippet = (
|
||||||
|
googleTagManagerId: string
|
||||||
|
) => `<!-- Google Tag Manager -->
|
||||||
|
(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.async=true;j.src=
|
||||||
|
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
||||||
|
})(window,document,'script','dataLayer','${googleTagManagerId}');
|
||||||
|
<!-- End Google Tag Manager -->`
|
||||||
|
|
||||||
|
export const gtmBodyElement = (googleTagManagerId: string) => {
|
||||||
|
if (document.getElementById('gtm-noscript')) return ''
|
||||||
|
const noScriptElement = document.createElement('noscript')
|
||||||
|
noScriptElement.id = 'gtm-noscript'
|
||||||
|
const iframeElement = document.createElement('iframe')
|
||||||
|
iframeElement.src = `https://www.googletagmanager.com/ns.html?id=${googleTagManagerId}`
|
||||||
|
iframeElement.height = '0'
|
||||||
|
iframeElement.width = '0'
|
||||||
|
iframeElement.style.display = 'none'
|
||||||
|
iframeElement.style.visibility = 'hidden'
|
||||||
|
noScriptElement.appendChild(iframeElement)
|
||||||
|
return noScriptElement
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ const metadataSchema = z.object({
|
|||||||
imageUrl: z.string().optional(),
|
imageUrl: z.string().optional(),
|
||||||
favIconUrl: z.string().optional(),
|
favIconUrl: z.string().optional(),
|
||||||
customHeadCode: z.string().optional(),
|
customHeadCode: z.string().optional(),
|
||||||
|
googleTagManagerId: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const settingsSchema = z.object({
|
export const settingsSchema = z.object({
|
||||||
|
|||||||
Reference in New Issue
Block a user