Install PodSaid on Next.js
Next.js is the most common React framework we see in customer
embeds, and it’s also the framework that trips up the most
first-time integrators. The reason is one thing: the widget uses
Web Components, which touch window and customElements at
register time — both of which don’t exist during server-side
rendering. If you naively drop the snippet into a Server
Component, Next.js will error out with ReferenceError: window is not defined at build time.
This page shows the correct pattern for both App Router (Next 13.4+, the current default) and Pages Router (legacy but still widely used).
Time: about 5 minutes.
What you need before you start:
- A PodSaid account with a feed set up (see Quickstart).
- Your
feed-idandpk_live_key from Console → Settings → Embed Code. - The domains you’ll embed on (localhost for dev, production URL, and any Vercel preview URL) added to Console → Settings → Allowed Origins. Vercel preview URLs are each their own origin — you likely want a wildcard entry there; see Allowed origins for how to request one.
The 60-second version — App Router
Create a small client component:
// components/PodSaidWidget.tsx
'use client';
import { useEffect } from 'react';
export default function PodSaidWidget() {
useEffect(() => {
const s = document.createElement('script');
s.src = 'https://widget.podsaid.com/v1/widgets.iife.js';
s.defer = true;
document.body.appendChild(s);
return () => { s.remove(); };
}, []);
return (
// @ts-expect-error — Web Component, see "TypeScript" below
<ask-pod-widget
feed-id={process.env.NEXT_PUBLIC_PODSAID_FEED_ID}
api-key={process.env.NEXT_PUBLIC_PODSAID_API_KEY}
/>
);
} Import it into any page or layout:
// app/sermons/page.tsx
import PodSaidWidget from '@/components/PodSaidWidget';
export default function SermonsPage() {
return (
<main>
<h1>Sermons</h1>
<PodSaidWidget />
</main>
);
} Add the env vars in .env.local:
NEXT_PUBLIC_PODSAID_FEED_ID=yourfeedslug
NEXT_PUBLIC_PODSAID_API_KEY=pk_live_xxxxxxxxxxxxxxxx If that just worked, skip to TypeScript setup and Verifying it works. If you’re on Pages
Router, or you want to use next/script, keep reading.
Alternative — using next/script
If you prefer Next’s built-in script loader (better for load-order control across a large app), split into two components:
// app/layout.tsx
import Script from 'next/script';
export default function RootLayout({ children }) {
return (
<html>
<body>
{children}
<Script
src="https://widget.podsaid.com/v1/widgets.iife.js"
strategy="lazyOnload"
/>
</body>
</html>
);
} // components/PodSaidWidget.tsx
'use client';
export default function PodSaidWidget() {
return (
// @ts-expect-error — Web Component
<ask-pod-widget
feed-id={process.env.NEXT_PUBLIC_PODSAID_FEED_ID}
api-key={process.env.NEXT_PUBLIC_PODSAID_API_KEY}
/>
);
} Strategy choice:
lazyOnload— recommended. Widget loads after your page is interactive. Users don’t wait on it.afterInteractive— loads sooner but blocks nothing important. Use if you want the widget to appear a beat faster.- Don’t use
beforeInteractive— it forces the widget into the critical rendering path with no upside.
Pages Router (Next 12 and earlier layouts)
Same idea, different files. Since Pages Router doesn’t have the 'use client' boundary, use next/dynamic with SSR disabled:
// components/PodSaidWidget.tsx
import { useEffect } from 'react';
function Widget() {
useEffect(() => {
const s = document.createElement('script');
s.src = 'https://widget.podsaid.com/v1/widgets.iife.js';
s.defer = true;
document.body.appendChild(s);
return () => { s.remove(); };
}, []);
return (
// @ts-expect-error — Web Component
<ask-pod-widget
feed-id={process.env.NEXT_PUBLIC_PODSAID_FEED_ID}
api-key={process.env.NEXT_PUBLIC_PODSAID_API_KEY}
/>
);
}
export default Widget; // pages/sermons.tsx
import dynamic from 'next/dynamic';
const PodSaidWidget = dynamic(() => import('@/components/PodSaidWidget'), {
ssr: false,
});
export default function SermonsPage() {
return (
<main>
<h1>Sermons</h1>
<PodSaidWidget />
</main>
);
} { ssr: false } is the critical piece. Without it, Next tries to
render <ask-pod-widget> on the server, doesn’t find customElements there, and errors.
TypeScript setup
The default TypeScript config for a Next project doesn’t know
about <ask-pod-widget> — it’s a custom element, not a React
component. You’ll see:
Property 'ask-pod-widget' does not exist on type 'JSX.IntrinsicElements'. Two fixes; pick one.
Quick fix — @ts-expect-error on the tag
Shown in the examples above. Zero setup, one line per usage. Fine for one or two placements.
Proper fix — declare the element in global.d.ts
Create types/global.d.ts (or add to your existing global types):
import type { DetailedHTMLProps, HTMLAttributes } from 'react';
declare global {
namespace JSX {
interface IntrinsicElements {
'ask-pod-widget': DetailedHTMLProps<
HTMLAttributes<HTMLElement> & {
'feed-id': string;
'api-key': string;
'api-url'?: string;
},
HTMLElement
>;
}
}
}
export {}; Reference it in tsconfig.json:
{
"include": ["next-env.d.ts", "types/**/*.ts", "**/*.ts", "**/*.tsx"]
} Now you can drop the @ts-expect-error comments and get real
autocomplete on the widget attributes.
Common Next.js gotchas
1. ReferenceError: window is not defined at build
Cause: You rendered <ask-pod-widget> from a Server Component
(App Router) or without ssr: false (Pages Router).
Fix: Mark the component with 'use client' (App Router) or
wrap with dynamic(…, { ssr: false }) (Pages Router). See the
examples above.
2. Widget renders twice in dev, once in prod
Cause: React’s Strict Mode double-invokes effects in
development to help catch bugs. Your useEffect appends the script
twice — once for each invocation.
Fix: The example above already handles this with the cleanup
function (return () => { s.remove(); }), but the script may
re-appear briefly during the double-invocation. It’s cosmetic and
gone in production. If it bothers you in dev, add a guard:
useEffect(() => {
if (document.querySelector('script[src*="widgets.iife.js"]')) return;
const s = document.createElement('script');
// ...
}, []); The next/script path (Alternative section) avoids this entirely —
Next dedupes script tags itself.
3. Environment variables aren’t reaching the browser
Symptom: Widget mounts but shows a [podsaid-widget] Auth failed error immediately, and DevTools shows feed-id="" or api-key="".
Cause: You named your env vars without the NEXT_PUBLIC_ prefix. Next only exposes variables with that prefix to the
browser — everything else is server-only.
Fix: Rename to NEXT_PUBLIC_PODSAID_FEED_ID and NEXT_PUBLIC_PODSAID_API_KEY in .env.local. Restart the dev
server (env changes don’t hot-reload).
4. Vercel preview URLs are 403-ing
Cause: Each preview deploy gets its own subdomain like yourapp-git-branch-team.vercel.app. Every one of them is a
separate origin. Your allowlist has your production domain but not
these ephemeral ones.
Fix: Two options:
- Recommended: Ask us for a wildcard origin entry like
https://*.vercel.appscoped to your team’s Vercel account. Emailsupport@podsaid.comwith your Vercel team slug — this is a one-time setup. - Quick: Add the preview URL manually each time you want to test. Fine for a first deploy; painful long-term.
Production URLs (your custom domain) are unaffected.
5. HMR errors about customElements.define being called twice
Symptom: After a hot reload in dev, browser console shows:
NotSupportedError: Failed to execute 'define' on 'CustomElementRegistry':
the name "ask-pod-widget" has already been used Cause: Hot Module Replacement re-runs your module, which re-runs the widget’s registration. The custom element registry rejects the second call.
Fix: Cosmetic — the widget still works. Ignore in dev.
Production builds don’t HMR, so it never appears there. If it
annoys you enough, use the next/script path (Alternative section)
which loads the widget from an external script tag; that only runs
once.
Verifying it works
Run
npm run devand open the page in the browser.Look for the widget in the location you placed it.
Open DevTools console. Look for:
[podsaid-widget] v1.8.2 mounted (feed=yourfeedslug, api=https://api.podsaid.com)Click the widget’s launcher. Ask a test question.
If the widget appears but questions fail, jump straight to Widget errors — what each one means.
Deploying
- Vercel — no special configuration. Just make sure your production URL is in the Allowed Origins list, and see gotcha #4 above for preview URLs.
- Netlify — same. Add your
*.netlify.appdomain and any custom domain. - Self-hosted / Docker — nothing widget-specific. The widget is
a browser-side load from
widget.podsaid.com; your server never touches it.
When the widget shows “still preparing your content”
Ingest hasn’t finished for your feed yet. The widget’s mount is correct — the content just isn’t ready. 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).
Still stuck?
- Errors index: Widget errors — what each one means
- Book a 15-minute call with Mark: /docs/support
- Email:
support@podsaid.com— include your Next.js version (App Router or Pages Router), a browser console screenshot, and a minimal reproduction if possible