once in your app's static HTML — index.html at the project root for Vite, public/index.html for Create React App. Do not import it from inside a React component; the widget uses side effects to register the custom element."},{"@type":"HowToStep","position":2,"name":"Add in your JSX","text":"In App.tsx or a route component, drop . Attributes must be kebab-case — camelCase does not pass through React’s attribute handling on custom elements before React 19."},{"@type":"HowToStep","position":3,"name":"Declare the custom element in a .d.ts file","text":"Create src/global.d.ts and add a JSX IntrinsicElements declaration for ask-pod-widget so TypeScript stops complaining. See the guide for the exact snippet — one shape for React 17/18, another for React 19."},{"@type":"HowToStep","position":4,"name":"Add localhost to Allowed Origins","text":"In Console → Settings → Allowed Origins, add http://localhost:5173 (Vite) or http://localhost:3000 (CRA). Leave it in the list permanently — costs nothing."},{"@type":"HowToStep","position":5,"name":"Run the dev server and verify","text":"Start the dev server (npm run dev or npm start), open the app, click the floating launcher, check DevTools console for the \"[podsaid-widget] mounted\" line."}]}PodSaid — AI-powered search and chat for podcasts and sermons
Skip to main content
Skip to main content

Install PodSaid in a React app

For Next.js, see the dedicated Next.js guide — the SSR / hydration story is different. This page is for client-rendered React apps: Vite, Create React App, or any plain-React SPA. If you’re using Remix or React Router v7 in SSR mode, follow the Next.js guide’s SSR advice.

The widget is not a React component — it’s a custom element registered by an IIFE script. React talks to it via ordinary DOM attributes.

Time: 5 minutes including TypeScript setup.

What you need before you start:

  • A PodSaid account with a feed set up (see Quickstart).
  • The embed snippet’s two values: your feed-id and pk_live_ key. Grab them from Console → Settings → Embed Code.
  • The domain(s) your app is served from added to your Allowed Origins. Include http://localhost:5173 (Vite) or http://localhost:3000 (CRA) for local dev — you can leave localhost in the allowlist indefinitely. See Allowed origins.

Step 1 — Load the widget script

Add the loader once in your app’s static HTML shell — not inside a React component.

Vite — edit index.html at the project root:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>My app</title>
  </head>
  <body>
    <div id="root"></div>
    <script src="https://widget.podsaid.com/v1/widgets.iife.js" defer></script>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

Create React App — edit public/index.html:

<body>
  <div id="root"></div>
  <script src="https://widget.podsaid.com/v1/widgets.iife.js" defer></script>
</body>

Why not import it in a component? The widget script uses side effects to register the <ask-pod-widget> custom element with the browser at load time. Importing it from inside a component couples that side effect to the component’s lifecycle, which fights React’s reconciler — you can end up with double-registration warnings, or with the element registering after the JSX tries to use it. Load it once, statically, in the HTML shell.


Step 2 — Use <ask-pod-widget> in JSX

Drop the element in your App.tsx (or wherever you want the widget mounted). Attributes are kebab-case:

export default function App() {
  return (
    <>
      {/* your app UI */}
      <ask-pod-widget
        feed-id="yourfeedslug"
        api-key="pk_live_xxxxxxxxxxxxxxxx"
      />
    </>
  );
}

The widget mounts a floating chat launcher (default: bottom-right). Its UI lives inside a Shadow DOM, so your app’s CSS can’t affect it and its CSS can’t leak into your app.

Why kebab-case attributes and not camelCase? React’s props → attributes conversion only kebab-cases well-known HTML attributes. For unknown custom-element attributes (any name with a hyphen or that React doesn’t recognize), it passes props through as-is. If you write feedId="...", React sets a feedId attribute on the element and the widget doesn’t see it. Write feed-id="..." and it works. React 19 relaxes this — camelCase is auto-converted for custom elements — but kebab-case works everywhere including older React versions.


Step 3 — TypeScript setup

Without a type declaration, TSX will complain that ask-pod-widget isn’t a valid JSX element. Add this to any .d.ts file in your project (src/global.d.ts is a common name):

// src/global.d.ts
import 'react';

declare module 'react' {
  namespace JSX {
    interface IntrinsicElements {
      'ask-pod-widget': React.DetailedHTMLProps<
        React.HTMLAttributes<HTMLElement> & {
          'feed-id': string;
          'api-key': string;
        },
        HTMLElement
      >;
    }
  }
}

If you’re on an older setup that puts JSX types in the global JSX namespace (Create React App, older Vite templates), use this form instead:

// src/global.d.ts
declare namespace JSX {
  interface IntrinsicElements {
    'ask-pod-widget': React.DetailedHTMLProps<
      React.HTMLAttributes<HTMLElement> & {
        'feed-id': string;
        'api-key': string;
      },
      HTMLElement
    >;
  }
}

TypeScript picks it up via your tsconfig.json include (already includes .d.ts files by default).


Where to mount the element

  • In App.tsx (top-level) — the floating launcher shows on every route. Recommended for most sites.
  • Inside a specific route component — restricts the widget to that route. Useful if you only want the widget on, say, a Sermons or Episodes page.

Router integration: none required. The widget mounts once and manages its own UI outside the React tree. Route changes (React Router, TanStack Router, whatever) don’t need to unmount or re-mount the widget. Don’t call useEffect cleanup on it.


Common React gotchas

1. Attribute name mismatch

Symptom: Widget appears but is stuck on the “still preparing” placeholder — or DevTools shows [podsaid-widget] Auth failed (400): missing_feed_id.

Cause: You wrote feedId="..." or apiKey="..." (camelCase) in JSX. React set them as feedId / apiKey attributes; the widget looks for feed-id / api-key.

Fix: Change to kebab-case. Or upgrade to React 19, which auto-converts camelCase attribute names for custom elements.

2. Widget script imported inside a component

Symptom: Console warnings about customElements.define() being called more than once, or the widget flickers on route change.

Cause: You added import 'https://widget.podsaid.com/...' or a <script> inside a React component, so React re-runs it on every mount. Custom-element registration is a one-time-per-name operation; running it twice throws or warns.

Fix: Move the <script src="..."> tag out of any React component and into index.html (see Step 1). Load it once.

3. Server-rendering hydration mismatch (Next.js / Remix)

Symptom: Hydration warning like “did not match server-rendered HTML”.

Cause: You’re on Next.js or Remix in SSR mode, and this guide doesn’t fit — the widget can’t render on the server (custom elements need document), and React’s hydrator flags the mismatch.

Fix: Follow the Next.js guide instead. It covers dynamic() with ssr: false, and the equivalent for Remix / React Router v7 SSR mode.

4. Local dev requests fail with a CORS error

Symptom: Widget mounts but questions fail with [podsaid-widget] Auth failed (403): origin_denied on http://localhost:5173 or http://localhost:3000.

Cause: Localhost is a distinct origin. It’s not in your Allowed Origins list.

Fix: In Console → Settings → Allowed Origins, add http://localhost:5173 (or your dev port). Leave it in the allowlist permanently — costs nothing.


Verifying it works

  1. Run your dev server (npm run dev for Vite, npm start for CRA) and open the app in a browser.

  2. Look for the floating launcher (default: bottom-right).

  3. Click it. If a chat box slides in, you’re done.

  4. Open DevTools console (⌥⌘I on Mac, F12 on Windows). Look for:

    [podsaid-widget] v1.8.2 mounted (feed=yourfeedslug, api=https://api.podsaid.com)

If the widget appears but questions fail, jump to Widget errors — what each one means.


When the widget shows “still preparing your content”

If ingest hasn’t finished for your feed yet, the widget renders a friendly placeholder instead of an error. Your paste is correct — you just don’t have content to answer questions against yet. Watch progress under Console → Feed → Ingest status, or wait for the “your widget is live” email.

See How ingest works (and why it takes hours).


Match your brand

The widget renders inside a Shadow DOM, so your site’s CSS can’t reach the widget and the widget’s CSS can’t leak into your site — nothing in your theme will accidentally break the widget’s look. To make it match your church or podcast brand, the widget exposes a set of CSS custom properties and Shadow Parts you can target from your own stylesheet.

See Customization (CSS) for the full list, with copy-paste examples.


Still stuck?