Where the snippet goes in your framework
The exact file for Next.js, React, Vue, Nuxt, SvelteKit, Astro, Laravel, WordPress, Shopify and Google Tag Manager.
It is the same script tag everywhere. The only question is which file your framework wants it in, so here is that file for the ones people ask about most.
Swap pk_live_your_site_key for your own, which is on Settings → Websites →
your site → Install.
Plain HTML
Before the closing body tag, on every page you want chat on.
<!-- ... your page ... -->
<script src="https://widget.talkfront.app/loader.js" data-site="pk_live_your_site_key" async></script>
</body>
</html>
Next.js
With the App Router, use next/script in your root layout. afterInteractive
is the right strategy, because chat should never compete with your page for
bandwidth.
// app/layout.tsx
import Script from 'next/script';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<Script
src="https://widget.talkfront.app/loader.js"
data-site="pk_live_your_site_key"
strategy="afterInteractive"
/>
</body>
</html>
);
}
On the Pages Router, put it in the custom document.
// pages/_document.tsx
import { Html, Head, Main, NextScript } from 'next/document';
export default function Document() {
return (
<Html lang="en">
<Head />
<body>
<Main />
<NextScript />
<script src="https://widget.talkfront.app/loader.js" data-site="pk_live_your_site_key" async />
</body>
</Html>
);
}
React (Vite, Create React App)
There is no need for a component or a useEffect. Put it in the HTML shell
and it loads once, rather than on every render.
<!-- index.html -->
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<script src="https://widget.talkfront.app/loader.js" data-site="pk_live_your_site_key" async></script>
</body>
Vue and Nuxt
For Nuxt, declare it in the config so it lands on every page.
// nuxt.config.ts
export default defineNuxtConfig({
app: {
head: {
script: [
{
src: 'https://widget.talkfront.app/loader.js',
'data-site': 'pk_live_your_site_key',
async: true,
tagPosition: 'bodyClose',
},
],
},
},
});
For a plain Vue app built with Vite, use the React approach above. It is the
same index.html.
SvelteKit
<!-- src/app.html -->
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
<script src="https://widget.talkfront.app/loader.js" data-site="pk_live_your_site_key" async></script>
</body>
Astro
Use is:inline so Astro leaves the tag alone instead of trying to bundle it.
---
// src/layouts/Layout.astro
---
<html lang="en">
<body>
<slot />
<script is:inline src="https://widget.talkfront.app/loader.js" data-site="pk_live_your_site_key" async></script>
</body>
</html>
If your layout uses Astro’s ClientRouter, read
single-page apps below: it replaces the body on every
navigation, and the widget needs one line to survive that.
Laravel
Put it in your main layout. Since Blade already knows who is signed in, this is a good place to pass the visitor’s details. Add a signature and they are verified too.
{{-- resources/views/layouts/app.blade.php --}}
@auth
<script
src="https://widget.talkfront.app/loader.js"
data-site="{{ config('services.talkfront.site_key') }}"
data-user-id="{{ auth()->id() }}"
data-user-name="{{ auth()->user()->name }}"
data-user-email="{{ auth()->user()->email }}"
async
></script>
@else
<script
src="https://widget.talkfront.app/loader.js"
data-site="{{ config('services.talkfront.site_key') }}"
async
></script>
@endauth
</body>
WordPress
Hook into wp_footer from a child theme or a small plugin. Editing a parent
theme works until it updates, and then it does not.
<?php
// functions.php (in a child theme)
add_action( 'wp_footer', function () {
$user = wp_get_current_user();
?>
<script
src="https://widget.talkfront.app/loader.js"
data-site="pk_live_your_site_key"
<?php if ( $user->ID ) : ?>
data-user-id="<?php echo esc_attr( $user->ID ); ?>"
data-user-name="<?php echo esc_attr( $user->display_name ); ?>"
data-user-email="<?php echo esc_attr( $user->user_email ); ?>"
<?php endif; ?>
async
></script>
<?php
} );
Shopify
Edit your theme’s theme.liquid and add the snippet before the closing body
tag. Liquid gives you the logged-in customer for free.
{% comment %} layout/theme.liquid, just before </body> {% endcomment %}
<script
src="https://widget.talkfront.app/loader.js"
data-site="pk_live_your_site_key"
{% if customer %}
data-user-id="{{ customer.id }}"
data-user-name="{{ customer.name }}"
data-user-email="{{ customer.email }}"
{% endif %}
async
></script>
Google Tag Manager
Create a Custom HTML tag with the snippet, and fire it on All Pages.
<script src="https://widget.talkfront.app/loader.js" data-site="pk_live_your_site_key" async></script>
Worth knowing: tag managers are a common target for ad blockers, so chat loaded this way goes missing for more visitors than chat loaded directly. If you can put the tag in your HTML instead, do.
Single-page apps
The widget lives outside your app’s render cycle, so there is nothing to mount or unmount. Two things it cannot work out by itself:
When somebody signs in or out. Tell it, or an agent keeps seeing the person who was signed in when the page loaded.
// Any router, any framework.
onSignIn((user) => {
Talkfront('identify', { id: user.id, name: user.name, email: user.email });
});
onSignOut(() => {
Talkfront('reset');
});
When your router replaces the whole <body>. Most do not, but some view
transition implementations, Astro’s ClientRouter among them, swap the body
element on every navigation, which throws the chat button away. The widget
does not rebuild itself, so carry it across:
document.addEventListener('astro:before-swap', (event) => {
const widget = document.getElementById('talkfront-widget');
if (widget) {
event.newDocument.body.appendChild(widget);
}
});
The chat window reloads when it is moved, which is a browser rule about iframes rather than anything we can avoid. The conversation comes back as it was, because it is attached to the visitor rather than to the page.
Full detail on passing and signing visitor details is in telling Talkfront who your logged-in users are.
Last updated 22 September 2026.