---
title: "Framework integrations"
description: "Where to put the CanaryLytics script tag in plain HTML, Vite, Next.js App Router, Astro and TanStack Start, plus the pre-init fallback for async script injection."
last_updated: "2026-07-30"
---

# Framework integrations

The snippet is a single `<script>` tag, so every framework integration is really just "where does this framework want head content." The one exception is frameworks that inject the tag asynchronously via JavaScript rather than rendering a literal `<script>` element up front. That case is covered in the Next.js tab below.

<Tabs>
  <TabItem label="HTML" icon="seti:html">
    ```html
    <!-- in <head>, or right before </body> -->
    <script src="https://cdn.analytics.canarycoders.es/lytics.js" data-domain="myapp.com" defer></script>
    ```
  </TabItem>

  <TabItem label="Vite SPA" icon="seti:vite">
    Add it directly to `index.html`. Vite serves that as static HTML, so the tag reaches the browser unmodified:

    ```html title="index.html"
    <head>
      <meta charset="UTF-8" />
      <script src="https://cdn.analytics.canarycoders.es/lytics.js" data-domain="myapp.com" defer></script>
    </head>
    ```
  </TabItem>

  <TabItem label="Next.js (App Router)">
    ```tsx title="app/layout.tsx"
    import Script from "next/script";

    export default function RootLayout({ children }: { children: React.ReactNode }) {
      return (
        <html lang="en">
          <body>
            <Script
              src="https://cdn.analytics.canarycoders.es/lytics.js"
              data-domain="myapp.com"
              strategy="afterInteractive"
            />
            {children}
          </body>
        </html>
      );
    }
    ```

    <Aside type="tip" title="If data-domain isn't picked up">
      `next/script` can insert the tag asynchronously depending on `strategy`, and the snippet only reads `data-*` attributes off `document.currentScript`, which is only reliably set for a synchronously-executing `<script>`. If pageviews don't show up, set the [pre-init global](/docs/tracking/script-reference/#programmatic-config-fallback) *before* the script runs instead. It takes precedence in exactly this situation:

      ```tsx
      <Script id="canarylytics-config" strategy="beforeInteractive">
        {`window.lytics = { domain: "myapp.com" };`}
      </Script>
      <Script src="https://cdn.analytics.canarycoders.es/lytics.js" strategy="afterInteractive" />
      ```
    </Aside>
  </TabItem>

  <TabItem label="Astro" icon="astro">
    ```astro title="src/layouts/Layout.astro"
    ---
    ---
    <html lang="en">
      <head>
        <script src="https://cdn.analytics.canarycoders.es/lytics.js" data-domain="myapp.com" defer></script>
      </head>
      <body>
        <slot />
      </body>
    </html>
    ```
  </TabItem>

  <TabItem label="TanStack Start">
    Add it to the root route's `head` config:

    ```tsx title="src/routes/__root.tsx"
    export const Route = createRootRoute({
      head: () => ({
        // ...meta, links, etc.
        scripts: [
          { src: "https://cdn.analytics.canarycoders.es/lytics.js", defer: true, "data-domain": "myapp.com" },
        ],
      }),
      // ...
    });
    ```
  </TabItem>
</Tabs>

For any framework not listed here: if it can render a literal `<script>` tag into the document `<head>`, this snippet works. It has zero dependencies and doesn't care what rendered the page around it.

## Related

<CardGrid>
  <LinkCard title="SSR apps" description="Rails, Django, Express, Remix, and other server-rendered frameworks." href="/docs/environments/ssr/" />
  <LinkCard title="SPAs & client-side routing" description="If the app uses a client-side router." href="/docs/environments/spas/" />
</CardGrid>

## Sitemap

- [Docs sitemap](https://analytics.canarycoders.es/docs/sitemap.md)
- [Full documentation as a single file](https://analytics.canarycoders.es/llms-full.txt)
