帮助中心Developer integrationInstall the feedback widget

Install the feedback widget

Install the widget SDK, connect guest or signed-in users, and handle authentication and integration errors.

最后更新 2026年9月21日

The widget brings FeedLog Agent into your product. Customers can ask questions, read cited help articles, submit or add to feedback through conversations, and track progress in My feedback. See Introduction to FeedLog Agent and Setting up and launching Agent for capabilities and knowledge preparation.

The integration requires one function call. The SDK is a loader: approximately
5 KB gzipped, with no runtime dependencies. It ships as ES modules with
TypeScript declarations. The SDK renders the launcher, unread badge and panel;
the feedback UI is hosted by FeedLog and loaded in an iframe. FeedLog delivers updates to the hosted interface; changes to SDK capabilities require updating the package installed in your product.

Want a coding agent to handle the integration?

Use the Widget integration prompt. Copy it
into a coding agent that has access to your project, and it will inspect the
codebase, implement the frontend and server changes, and verify the result.

Install

bash 复制代码
npm install @feedlog/widget

ESM only

The package ships ES modules and type declarations and requires a bundler. It
does not provide a <script src="…"> build or a global variable, so direct
installation through a CMS code snippet is not supported.

Configure the widget

Choose the approach your product needs. Guest mode only requires the workspace URL. Existing-account integration also requires a backend token endpoint and a connection to your frontend sign-in state.

Guest-only integration

Enable guest posting in the workspace, then initialize the widget on the client:

ts 复制代码
import { createWidget } from '@feedlog/widget'

createWidget({
  baseUrl: 'https://acme.feedlog.ai',
  theme: 'auto',
})

Guest voting and commenting are controlled separately in workspace settings. The getToken and sign-in sections apply to existing-account integration; guest-only integrations can continue with the client initialization, appearance, enabling, and troubleshooting sections below.

Integrate existing product accounts

Pair the following frontend configuration with the backend token endpoint described later on this page:

ts 复制代码
import { createWidget } from '@feedlog/widget'

createWidget({
  baseUrl: 'https://acme.feedlog.ai',
  auth: {
    getToken: async () => {
      const res = await fetch('/api/feedlog-token')
      // A reachability problem, not a sign-out — see below.
      if (!res.ok) throw new Error('token endpoint unavailable')
      return (await res.json()).token ?? null
    },
    // Must resolve when the modal closes, not when it opens.
    login: () => openYourSignInModal(),
  },
  theme: 'auto',
})

createWidget is the package's only export. It has no return value, instance
methods or events
. The launcher opens the panel, and the panel's close button
closes it. The widget manages this state internally, which allows its behaviour
to change without requiring changes to your integration code.

Options

Option Required Notes
baseUrl Yes Your FeedLog address, e.g. https://acme.feedlog.ai. A custom domain works the same way.
auth.getToken When auth is supplied () => Promise<string | null>. Reports the current sign-in state. Review the three possible outcomes below before implementing it.
auth.login No () => void | Promise<void>. Opens your own sign-in UI when someone asks to sign in from inside the panel.
theme No 'light', 'dark' or 'auto' (default). Follows the OS on auto.

For guest-only integration, omit auth and enable guest posting in the workspace.
A product with its own accounts can supply auth to pass signed-in identities.

getToken has three outcomes, not two

getToken() reports the current sign-in state. FeedLog handles its three
possible outcomes differently:

Your getToken() FeedLog reads it as What the widget does
resolves with a JWT Signed in, as the email in that token Exchanges it for a session and opens the panel as that user
resolves with null Signed out Clears the cached signed-in session; allows guest input when guest posting is enabled, otherwise shows a sign-in prompt
rejects or throws Temporary failure Keeps the session; offers a retry, or leaves a working panel alone

Never return null for a failure

null means that the user is signed out. Return it only when you know that no
user is signed in. If the token route returns a 500 response, the request times
out or fetch rejects, throw an error.

Returning null for a temporary failure clears the cached session. This can
cause intermittent sign-outs with little diagnostic evidence because the
original network error is no longer reported as an error.

getToken() runs when the panel is opened for the first time and on every
subsequent open. This keeps the widget synchronized with your app's sign-in
state, so the function should complete quickly. The SDK caches the FeedLog
session obtained from the JWT; subsequent opens normally call your function
without making another exchange request.

Opening your own sign-in UI

auth.login is optional. It is called when a user selects sign-in in the panel
while getToken() returns null. It can open your existing sign-in UI, such as
a modal, popup or full-page redirect to your identity provider. For a full-page
redirect, the SDK stores a short-lived marker and reopens the panel when the user
returns to the page, provided the round trip takes less than approximately five
minutes.

A settled login() call does not indicate successful sign-in. It only tells
the SDK that the interaction has ended. The SDK then calls getToken() again and
uses that result to determine the sign-in state. When the user cancels, resolving
and rejecting have the same effect. You do not need to return the outcome of the
sign-in attempt.

An expired session does not call login() automatically. The SDK first retries
getToken() without opening any UI because the user may have signed in in
another tab. The sign-in UI opens only after the user explicitly selects sign-in
inside the panel; it will not appear while the user is only reading the page.

Sign the token on your server

Your backend must sign the JWT returned by getToken() with an SSO secret from
Developer → Single Sign-On. Use HS256 and the four supported claims, with
exp set no more than 24 hours in the future:

ts 复制代码
// Server-side only — the secret must never reach the browser.
import jwt from 'jsonwebtoken'

export default handler(async (req) => {
  const user = await currentUser(req)      // your own session, however you read it
  if (!user) return { token: null }        // becomes `null` in getToken()

  return {
    token: jwt.sign(
      { email: user.email, name: user.name, picture: user.avatarUrl },
      process.env.FEEDLOG_SSO_SECRET,
      { algorithm: 'HS256', expiresIn: '1h' },
    ),
  }
})

The SDK posts the token to FeedLog, caches the returned session in localStorage
under the signed-in email address, and exchanges the token again when the session
expires. You do not call the exchange endpoint directly. The session is passed
to the iframe in the URL fragment, keeping it out of access logs and Referer
headers.

For secret creation and rotation, the full claim list and all exchange errors,
see Single sign-on (JWT handoff). If browser handoff is already
configured in your product, reuse the same signing code, secret and token.

Where to call it

Call createWidget() once per page on the client. It does nothing when window
is unavailable, so it can be imported in a Nuxt or Next app. Place the call in a
client-only lifecycle hook such as onMounted or useEffect, or in a .client
plugin.

A second call logs a warning and returns without creating another widget. This
prevents React StrictMode double mounts and HMR module reloads from adding a
second launcher. Calling the function again does not update baseUrl or theme;
reload the page to change either option.

DOM structure and appearance

The SDK adds one <div> with a shadow root to document.body. It contains the
launcher, badge and panel. The shadow root prevents the host page's CSS resets
from affecting the widget and prevents widget styles from affecting the host
page; no additional z-index configuration is required. The launcher appears in
the bottom-right corner. The panel is 400 px wide on desktop and becomes
full-screen when the viewport is narrower than 520 px.

The brand colour comes from your FeedLog workspace, not from the SDK. Configure
it in workspace settings. FeedLog also selects a readable foreground colour.
theme is the SDK's only appearance option. It is passed to the iframe when the
frame is created, before the first paint, so provide the final value in the
createWidget() call. Resolving it later from a store or a media-query listener
that runs after mount can cause the panel to display the wrong theme briefly.
Use auto unless your app has its own light/dark toggle; auto follows the OS
and updates when the OS setting changes.

The badge shows the number of unread replies. While the panel is closed, the SDK
refreshes the count on page load and when the tab regains focus, subject to a
60-second cache. When the panel is open, the iframe updates the count and clears
items as their threads are read. The displayed count is capped at 9+.

Enable the widget

Enable the widget in Settings → Widget. The SDK checks this setting before it
renders. When the widget is disabled, it renders nothing and logs nothing. The
setting applies to every site where the SDK is installed and does not require a
new deployment of your product.

No domain allowlist is required. FeedLog does not maintain a registry of customer
domains: the widget API accepts requests from any origin, and any site can frame
the embed page. Authentication does not rely on cookies; the session is supplied
as an explicit bearer token.

The widget supports current versions of Chrome, Firefox, Safari and Edge. The
build targets ES2020 and uses Shadow DOM, fetch and Web Storage without
polyfills. If Web Storage throws in private browsing mode, the SDK uses in-memory
storage instead. The session cache then does not survive page reloads, resulting
in one additional token exchange per page load.

Common errors you may encounter

Two launchers on the page. The guard covers a second call in the same page
load, and prints createWidget() was already called; ignoring this call when it
fires. Two launchers without this warning indicate two documents, such as an
iframe or micro-frontend that loads the integration a second time.

Users are signed out intermittently. getToken() is returning null on a
temporary failure instead of throwing. See the three outcomes above.

createWidget requires a baseUrl, or auth.getToken must be a function (TypeError).
These errors are thrown synchronously and indicate that an option is missing or
has the wrong type. getToken must be a function, not a promise or token string.
baseUrl is not a valid URL means the value could not be parsed; include the
scheme.

Nothing renders, and the console says could not load widget config. The SDK
could not reach GET /api/widget/config at your baseUrl, so it cannot determine
whether the widget is enabled and does not render the launcher. Open that URL
directly. The same symptom can result from an incorrect baseUrl, a host that
does not resolve to a workspace or an unavailable FeedLog instance.

Nothing renders, and there's no warning either. Either the widget is switched
off in Settings → Widget, or the call only ever ran on the server. Confirm
the client reached it by putting a console.log next to it.

The panel continues to show a loading spinner. The iframe loaded but did not
report itself ready. Check the embed page request in the network panel. A 404 or
500 response renders as an empty frame rather than an error because FeedLog's
error pages do not include the header that allows framing.

"Feedback could not be loaded." with a Try again button. getToken() threw,
or the exchange failed. The status is in the console as
Widget token exchange failed with status …; the reason is JSON in that
response body, so read the request in the network panel. A 403 there means the
widget is disabled, and a 400 usually indicates a token problem. The
SSO page lists the exact messages.

A sign-in prompt inside the panel, for a user who is signed in to your app.
getToken() is returning null. Most often the token route can't see your
session cookie: a different subdomain, or a SameSite setting that drops it.
Call the route from the browser console and check what it actually returns.

The badge is up to one minute out of date. The count is cached for 60 seconds
while the panel is closed. Opening the panel refreshes the count.

Agent page context

The updated SDK reads the current pathname, document.title and meta description for each message and sends them to the trusted iframe. It excludes query parameters, URL fragments and the page body. No extra configuration is needed; SPA navigation is reflected in the next message. Older SDK releases still support chat without page context.