2
0

Add Google Tag Manager (#185)

This commit is contained in:
Jorgelig
2022-12-20 09:59:18 -06:00
committed by GitHub
parent 3394fa5e0a
commit a898a7aa41
8 changed files with 129 additions and 80 deletions

View File

@ -36,6 +36,8 @@ export const MetadataForm = ({
onMetadataChange({ ...metadata, favIconUrl })
const handleImageSubmit = (imageUrl: string) =>
onMetadataChange({ ...metadata, imageUrl })
const handleGoogleTagManagerIdChange = (googleTagManagerId: string) =>
onMetadataChange({ ...metadata, googleTagManagerId })
const handleHeadCodeChange = (customHeadCode: string) =>
onMetadataChange({ ...metadata, customHeadCode })
@ -92,26 +94,23 @@ export const MetadataForm = ({
</PopoverContent>
</Popover>
</Stack>
<Stack>
<FormLabel mb="0" htmlFor="title">
Title:
</FormLabel>
<Input
id="title"
defaultValue={metadata.title ?? typebotName}
onChange={handleTitleChange}
/>
</Stack>
<Stack>
<FormLabel mb="0" htmlFor="description">
Description:
</FormLabel>
<Textarea
id="description"
defaultValue={metadata.description}
onChange={handleDescriptionChange}
/>
</Stack>
<Input
label="Title:"
defaultValue={metadata.title ?? typebotName}
onChange={handleTitleChange}
/>
<Textarea
defaultValue={metadata.description}
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>
<HStack as={FormLabel} mb="0" htmlFor="head">
<Text>Custom head code:</Text>

View File

@ -101,13 +101,10 @@ test.describe.parallel('Settings page', () => {
await page.fill('input[placeholder="Paste the image link..."]', imageUrl)
await expect(websiteImg).toHaveAttribute('src', imageUrl)
// Title
await page.fill('input#title', 'Awesome typebot')
// Description
await page.fill('textarea#description', 'Lorem ipsum')
// Custom head code
await page.getByRole('textbox', { name: 'Title' }).fill('Awesome typebot')
await page
.getByRole('textbox', { name: 'Description' })
.fill('Lorem ipsum')
await page.fill(
'div[contenteditable=true]',
'<script>Lorem ipsum</script>'

View File

@ -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.
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.

View File

@ -1,6 +1,9 @@
import { gtmHeadSnippet } from '@/lib/google-tag-manager'
import { Metadata } from 'models'
import Head from 'next/head'
import Script from 'next/script'
import React from 'react'
import { isNotEmpty } from 'utils'
type SEOProps = {
url: string
@ -11,55 +14,62 @@ type SEOProps = {
export const SEO = ({
url,
typebotName,
metadata: { title, description, favIconUrl, imageUrl },
metadata: { title, description, favIconUrl, imageUrl, googleTagManagerId },
}: SEOProps) => (
<Head>
<title>{title ?? typebotName}</title>
<meta name="robots" content="noindex" />
<link
rel="icon"
type="image/png"
href={favIconUrl ?? 'https://bot.typebot.io/favicon.png'}
/>
<meta name="title" content={title ?? typebotName} />
<meta
name="description"
content={
description ??
'Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form.'
}
/>
<>
<Head key="seo">
<title>{title ?? typebotName}</title>
<meta name="robots" content="noindex" />
<link
rel="icon"
type="image/png"
href={favIconUrl ?? 'https://bot.typebot.io/favicon.png'}
/>
<meta name="title" content={title ?? typebotName} />
<meta
name="description"
content={
description ??
'Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form.'
}
/>
<meta property="og:type" content="website" />
<meta property="og:url" content={url ?? 'https://bot.typebot.io'} />
<meta property="og:title" content={title ?? typebotName} />
<meta property="og:site_name" content={title ?? typebotName} />
<meta
property="og:description"
content={
description ??
'Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form.'
}
/>
<meta
property="og:image"
itemProp="image"
content={imageUrl ?? 'https://bot.typebot.io/site-preview.png'}
/>
<meta property="og:type" content="website" />
<meta property="og:url" content={url ?? 'https://bot.typebot.io'} />
<meta property="og:title" content={title ?? typebotName} />
<meta property="og:site_name" content={title ?? typebotName} />
<meta
property="og:description"
content={
description ??
'Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form.'
}
/>
<meta
property="og:image"
itemProp="image"
content={imageUrl ?? 'https://bot.typebot.io/site-preview.png'}
/>
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={url ?? 'https://bot.typebot.io'} />
<meta property="twitter:title" content={title ?? typebotName} />
<meta
property="twitter:description"
content={
description ??
'Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form.'
}
/>
<meta
property="twitter:image"
content={imageUrl ?? 'https://bot.typebot.io/site-preview.png'}
/>
</Head>
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={url ?? 'https://bot.typebot.io'} />
<meta property="twitter:title" content={title ?? typebotName} />
<meta
property="twitter:description"
content={
description ??
'Build beautiful conversational forms and embed them directly in your applications without a line of code. Triple your response rate and collect answers that has more value compared to a traditional form.'
}
/>
<meta
property="twitter:image"
content={imageUrl ?? 'https://bot.typebot.io/site-preview.png'}
/>
</Head>
{isNotEmpty(googleTagManagerId) && (
<Script id="google-tag-manager">
{gtmHeadSnippet(googleTagManagerId)}
</Script>
)}
</>
)

View File

@ -2,11 +2,12 @@ import { TypebotViewer } from 'bot-engine'
import { AnswerInput, PublicTypebot, Typebot, VariableWithValue } from 'models'
import { useRouter } from 'next/router'
import React, { useEffect, useState } from 'react'
import { isDefined, isNotDefined } from 'utils'
import { isDefined, isNotDefined, isNotEmpty } from 'utils'
import { SEO } from './Seo'
import { ErrorPage } from './ErrorPage'
import { createResultQuery, updateResultQuery } from '@/features/results'
import { upsertAnswerQuery } from '@/features/answers'
import { gtmBodyElement } from '@/lib/google-tag-manager'
export type TypebotPageProps = {
publishedTypebot: Omit<PublicTypebot, 'createdAt' | 'updatedAt'> & {
@ -51,6 +52,8 @@ export const TypebotPage = ({
initializeResult().then()
if (isDefined(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
}, [])

View File

@ -125,12 +125,14 @@ test('Show close message', async ({ page }) => {
test('Should correctly parse metadata', async ({ page }) => {
const typebotId = cuid()
const googleTagManagerId = 'GTM-M72NXKB'
const customMetadata: Metadata = {
description: 'My custom description',
title: 'Custom title',
favIconUrl: 'https://www.baptistearno.com/favicon.png',
imageUrl: 'https://www.baptistearno.com/images/site-preview.png',
customHeadCode: '<meta name="author" content="John Doe">',
googleTagManagerId,
}
await createTypebots([
{
@ -146,6 +148,11 @@ test('Should correctly parse metadata', async ({ page }) => {
},
])
await page.goto(`/${typebotId}-public`)
await expect(
typebotViewer(page).locator(
`input[placeholder="${defaultTextInputOptions.labels.placeholder}"]`
)
).toBeVisible()
expect(
await page.evaluate(`document.querySelector('title').textContent`)
).toBe(customMetadata.title)
@ -177,9 +184,12 @@ test('Should correctly parse metadata', async ({ page }) => {
.content
)
).toBe('John Doe')
await expect(
typebotViewer(page).locator(
`input[placeholder="${defaultTextInputOptions.labels.placeholder}"]`
expect(
await page.evaluate(
(googleTagManagerId) =>
document.querySelector(
`iframe[src="https://www.googletagmanager.com/ns.html?id=${googleTagManagerId}"]`
) as HTMLMetaElement
)
).toBeVisible()
).toBeDefined()
})

View 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
}

View File

@ -21,6 +21,7 @@ const metadataSchema = z.object({
imageUrl: z.string().optional(),
favIconUrl: z.string().optional(),
customHeadCode: z.string().optional(),
googleTagManagerId: z.string().optional(),
})
export const settingsSchema = z.object({