USE CASE

SvelteKit AI support chat widget

Add Deacon to a SvelteKit app with one line in src/app.html. It answers visitors from your docs and says so when it doesn’t know.

By the Deacon team

Published

Deacon is an AI support chat that a SvelteKit app gets from one line in src/app.html. Ask it something your docs or pages explain and the answer comes back in seconds. Ask it something they don’t, and it says so instead of guessing, with a box to leave an email underneath, or a one-tap reply for a logged-in user your layout has passed it. Once they leave it, and unless you’re in the dashboard already, an email tells you what they asked, and you reply from the dashboard into their chat. Deacon’s free plan gives 50 answers a month with no card and no time limit.

On 25 October 2026, when we read the docs, SvelteKit was at 3.1 and Svelte at 5.57. For coding agents, the same page is in Markdown.

You’ll need

Start a Deacon account, and the Install page gives you a snippet with your pk_ key as its data-key. The domain the app is deployed to must be on Install’s list, which already holds the one you signed up with and its subdomains. Nothing gets added to package.json.

Install recognises a SvelteKit build from your home page and says so. Its Copy the prompt hands Claude Code, Cursor or Codex the tag with your key, and the agent edits the project and checks it’s live.

src/app.html, the template every page shares

SvelteKit’s project structure docs call src/app.html “your page template”, and every page is rendered into it. That makes it the one file to edit. The Deacon <script> line is new, and the rest is the default template that npx sv create writes (sv 1.1.1).

<!-- src/app.html -->
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="text-scale" content="scale" />
    %sveltekit.head%
  </head>
  <body data-sveltekit-preload-data="hover">
    <div style="display: contents">%sveltekit.body%</div>
    <script src="https://heydeacon.com/widget/v1/loader.js" data-key="pk_your_widget_key" async></script>
  </body>
</html>

Keep it outside %sveltekit.body%

Put the tag after the element that wraps %sveltekit.body%, never inside it. The same docs explain why the placeholder sits in an element of its own rather than straight in <body>. Elements that aren’t part of your app can otherwise be “destroyed by the hydration process”. Deacon’s script also puts its chat outside <body> entirely, so nothing SvelteKit renders can remove it.

Seeing it in dev and preview

SvelteKit’s dev server and its preview of a build run on different ports, 5173 and 4173. The one-click add lists localhost:5173 only, so type localhost under Your sites on Install, which covers both, or add localhost:4173 when Install names it.

  1. npm run dev starts the app on localhost:5173. It loads without a chat button the first time, because localhost isn’t on the list.
  2. Install spots it within seconds, saying “Found the code on localhost:5173”, and allows it with one click.
  3. Refresh. The button, Help unless your Appearance label says otherwise, is in the corner. Try a question your docs answer, which on localhost costs nothing.
  4. npm run build, then npm run preview, and check the built app on port 4173 the same way.

When the deploy lands, Install calls the domain Live with when Deacon was last seen. On the live site, after the first page, SvelteKit’s client-side app takes over, and Deacon counts each route it moves to as a page view.

From a terminal, an agent can ask Deacon directly. latest is the most recent address to load the script and refused a development address it declined, as Deacon’s install guide sets out.

curl -s "https://heydeacon.com/api/widget/seen?k=pk_your_widget_key"

Try Deacon for free

Install Deacon

Prerendered and static builds

src/app.html is the template for prerendered pages too, so a site built with adapter-static, or with every route prerendered, carries the tag on each page with no further step. Only the logged-in code below needs a server.

The logged-in user, from locals to the layout

If people log in, pass Deacon who each one is. Their name then labels the conversation, and a question Deacon can’t answer gets a reply to their email in one tap. Deacon doesn’t verify the details or show them to the AI.

Return just the three fields from the root layout’s server load, reading the user your handle hook stores on locals, as SvelteKit’s docs on loading data do.

// src/routes/+layout.server.js
export function load({ locals }) {
  const user = locals.user;

  return {
    user: user ? { id: user.id, email: user.email, name: user.name } : null,
  };
}

Then pass them on in the root layout with an effect. Svelte’s $effect runs “only in the browser, not during server-side rendering”, and runs again when data.user changes, as it does when a form action logs someone in or out.

<!-- src/routes/+layout.svelte -->
<script>
  let { data, children } = $props();

  $effect(() => {
    window.deaconUser = data.user;
    window.Deacon?.identify(window.deaconUser);
  });
</script>

{@render children()}

The effect may run before the script has loaded or after it, and setting the variable as well as calling identify handles both. In TypeScript, declare window.deaconUser and window.Deacon in src/app.d.ts, using the declaration in Deacon’s install guide.

What the chat tells you

Visitors get answers without filing a ticket. You get to read what they asked. Every question Deacon couldn’t answer waits for your reply under Answer this, and Topics counts the people behind each subject. What people ask you to build is listed too, in their own words.

SvelteKit questions

Where’s the chat button?

Look in the browser console for a line from [Deacon]. “isn’t in your allowed domains” gives the address to add on Install, a preview deploy perhaps, “Unknown widget key” means data-key isn’t the one on your snippet, and “could not start” comes with the error that stopped it. If nothing from Deacon appears, view the page source for loader.js. If it’s missing, the tag isn’t in src/app.html, and if it’s there, the button may be hidden in Appearance. When the button shows but the chat opens blank, a line about the chat frame that “did not load” means a Content Security Policy, perhaps from SvelteKit’s csp option, without https://heydeacon.com in frame-src.

How do I open the chat from my own button?

Hide Deacon’s button in Appearance and use onclick={() => window.Deacon?.open()} on yours.

Try it on your own documentation

Add your help pages, ask it the question you know your docs cannot answer, and watch it say so.

The free plan covers 50 answers a month on one seat, and the paid plans are on the pricing page.

Start free

Free plan, no card.

READ NEXT