Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 32 additions & 38 deletions docs/content/docs/plugins/comments.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -243,17 +243,14 @@ overrides={{
## Embedding Comments

The `CommentThread` component can be embedded anywhere — below a blog post, inside a Kanban task dialog, or on a custom page.
It uses the `api` and `auth` values from the nearest `StackProvider`.

```tsx
import { CommentThread } from "@btst/stack/plugins/comments/client/components"

<CommentThread
resourceId={post.slug} // Unique identifier for the resource being commented on
resourceType="blog-post" // Namespace — avoids ID collisions across resource types
apiBaseURL="https://example.com"
apiBasePath="/api/data"
currentUserId={session?.user?.id} // Own-comment affordances (edit, delete, pending badge, dedup likes)
loginHref="/login" // Shows "Please log in to comment" when currentUserId is not set
components={{
// Optional: replace the plain textarea with a rich editor
Input: MarkdownEditor,
Expand All @@ -269,10 +266,10 @@ import { CommentThread } from "@btst/stack/plugins/comments/client/components"
|------|------|----------|-------------|
| `resourceId` | `string` | ✓ | Identifier for the resource (e.g. post slug, task ID) |
| `resourceType` | `string` | ✓ | Type of resource (`"blog-post"`, `"kanban-task"`, etc.) |
| `apiBaseURL` | `string` | | Base URL for API requests |
| `apiBasePath` | `string` | | Path prefix where the API is mounted |
| `currentUserId` | `string` | — | Authenticated user ID — enables edit/delete/pending badge |
| `loginHref` | `string` | — | Login page URL shown to unauthenticated users |
| `apiBaseURL` | `string` | | Explicit base URL override; defaults to `StackProvider.api.baseURL` |
| `apiBasePath` | `string` | | Explicit API path override; defaults to `StackProvider.api.basePath` |
| `currentUserId` | `string` | — | Explicit identity override; defaults to `StackProvider.auth` identity |
| `loginHref` | `string` | — | Explicit login URL override; defaults to `StackProvider.auth.loginPath` |
| `pageSize` | `number` | — | Comments per page. Falls back to `defaultCommentPageSize` from overrides, then 100. A "Load more" button appears when there are additional pages. |
| `sort` | `"asc" \| "desc"` | — | Sort direction for top-level comments by `createdAt`. Defaults to `defaultCommentSort` from overrides, then `"desc"` (newest first). Replies inside each thread always render chronologically and are unaffected. |
| `components.Input` | `ComponentType` | — | Custom input component (default: `<textarea>`) |
Expand All @@ -291,10 +288,6 @@ overrides={{
<CommentThread
resourceId={post.slug}
resourceType="blog-post"
apiBaseURL={baseURL}
apiBasePath="/api/data"
currentUserId={session?.user?.id}
loginHref="/login"
/>
),
}
Expand All @@ -314,10 +307,6 @@ overrides={{
<CommentThread
resourceId={task.id}
resourceType="kanban-task"
apiBaseURL={baseURL}
apiBasePath="/api/data"
currentUserId={session?.user?.id}
loginHref="/login"
/>
),
}
Expand Down Expand Up @@ -451,32 +440,37 @@ The comments plugin registers a `/comments` route that shows the current user's
- Prev / Next pagination (20 per page)
- Resource link column — click through to the original resource when `resourceLinks` is configured (links automatically include `#comments` so the page scrolls to the comment thread)
- Delete button with confirmation dialog — calls `DELETE /comments/:id` (governed by `onBeforeDelete`)
- Login prompt when `currentUserId` is not configured
- Login prompt when the top-level auth provider resolves no identity

### Setup

Configure the overrides in your layout and the security hook in your backend:
Configure top-level API/auth once, keep only comments-specific overrides in
your layout, and configure the security hook in your backend:

```tsx title="app/pages/layout.tsx"
overrides={{
comments: {
apiBaseURL: baseURL,
apiBasePath: "/api/data",

// Provide the current user's ID so the page can scope the query
currentUserId: session?.user?.id,
<StackProvider
api={{ baseURL, basePath: "/api/data" }}
auth={{
getIdentity: async () => (await getSession())?.user ?? null,
loginPath: "/login",
}}
overrides={{
comments: {

// Map resource types to URLs so comments link back to their resource
resourceLinks: {
"blog-post": (slug) => `/pages/blog/${slug}`,
"kanban-task": (id) => `/pages/kanban?task=${id}`,
},
// Map resource types to URLs so comments link back to their resource
resourceLinks: {
"blog-post": (slug) => `/pages/blog/${slug}`,
"kanban-task": (id) => `/pages/kanban?task=${id}`,
},

onBeforeUserCommentsPageRendered: (context) => {
if (!session?.user) throw new Error("Authentication required")
onBeforeUserCommentsPageRendered: (context) => {
if (!session?.user) throw new Error("Authentication required")
},
},
}
}}
}}
>
{children}
</StackProvider>
```

```ts title="lib/stack.ts"
Expand Down Expand Up @@ -522,12 +516,12 @@ Configure the comments plugin behavior from your layout:
| Field | Type | Description |
|-------|------|-------------|
| `localization` | `Partial<CommentsLocalization>` | Override any UI string in the plugin. Import `COMMENTS_LOCALIZATION` from `@btst/stack/plugins/comments/client` to see all available keys. |
| `apiBaseURL` | `string` | Base URL for API requests |
| `apiBasePath` | `string` | Path prefix for the API |
| `apiBaseURL` | `string` | Legacy per-plugin base URL override; prefer top-level `StackProvider.api`. |
| `apiBasePath` | `string` | Legacy per-plugin API path override; prefer top-level `StackProvider.api`. |
| `headers` | `Record<string, string>` | Optional headers for authenticated plugin API calls. |
| `showAttribution` | `boolean` | Show/hide the "Powered by BTST" attribution on plugin pages (defaults to `true`). |
| `currentUserId` | `string \| (() => string \| undefined \| Promise<string \| undefined>)` | Authenticated user's ID — used by the User Comments page. Supports async functions for session-based resolution. |
| `loginHref` | `string` | Login route used by comment UIs when user is unauthenticated. |
| `currentUserId` | `string \| (() => string \| undefined \| Promise<string \| undefined>)` | Legacy identity override. When omitted, comments use the top-level auth provider identity. |
| `loginHref` | `string` | Legacy login route override. When omitted, comments use `StackProvider.auth.loginPath`. |
| `defaultCommentPageSize` | `number` | Default number of top-level comments per page for all `CommentThread` instances. Overridden per-instance by the `pageSize` prop. Defaults to `100` when not set. |
| `defaultCommentSort` | `"asc" \| "desc"` | Default sort direction for top-level comments in all `CommentThread` instances. Overridden per-instance by the `sort` prop. Defaults to `"desc"` (newest first). |
| `allowPosting` | `boolean` | Hide/show comment form and reply actions globally in `CommentThread` instances (defaults to `true`). |
Expand Down
4 changes: 2 additions & 2 deletions e2e/tests/smoke.comments.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -690,8 +690,8 @@ test.describe("Own pending comments — visible after refresh (server-side fix)"
// ─── My Comments Page ────────────────────────────────────────────────────────
//
// The example app's onBeforePost returns authorId "olliethedev" for every POST,
// and the layout wires currentUserId: "olliethedev". All tests in this block
// rely on that fixture so they can verify comments appear on the my-comments page.
// and its top-level client auth provider resolves the same identity. All tests
// in this block rely on that fixture to verify the my-comments page.

test.describe("My Comments Page", () => {
const AUTHOR_ID = "olliethedev";
Expand Down
18 changes: 4 additions & 14 deletions packages/cli/src/templates/nextjs/form-demo-page.tsx.hbs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"use client"

import { useState } from "react"
import { useParams, useRouter } from "next/navigation"
import Link from "next/link"
import { useParams } from "next/navigation"
import { QueryClientProvider } from "@tanstack/react-query"
import { StackProvider } from "@btst/stack/context"
import { nextRouter } from "@btst/stack/next"
import { FormRenderer } from "@btst/stack/plugins/form-builder/client/components"
import type { FormBuilderPluginOverrides } from "@btst/stack/plugins/form-builder/client"
import { getOrCreateQueryClient } from "{{alias}}lib/query-client"
Expand All @@ -25,7 +25,6 @@ type PluginOverrides = {
*/
export default function FormDemoPage() {
const params = useParams()
const router = useRouter()
const slug = params.slug as string
const [queryClient] = useState(() => getOrCreateQueryClient())
const baseURL = getBaseURL()
Expand All @@ -34,17 +33,8 @@ export default function FormDemoPage() {
<QueryClientProvider client={queryClient}>
<StackProvider<PluginOverrides>
basePath=""
overrides={
{
"form-builder": {
apiBaseURL: baseURL,
apiBasePath: "/api/data",
navigate: (path) => router.push(path),
refresh: () => router.refresh(),
Link: ({ href, ...props }) => <Link href={href || "#"} {...props} />,
},
}
}
router={nextRouter()}
api={{{providerApiLiteral}}}
>
<main className="container mx-auto px-4 py-8">
<div className="max-w-2xl mx-auto">
Expand Down
19 changes: 8 additions & 11 deletions packages/cli/src/templates/nextjs/pages-layout.tsx.hbs
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
"use client"

{{#if pagesLayoutOverrides}}
import { StackProvider } from "@btst/stack/context"
{{/if}}
import { nextRouter } from "@btst/stack/next"
{{#if hasAiChat}}
import { ChatLayout } from "@btst/stack/plugins/ai-chat/client"
{{/if}}
import { QueryClientProvider } from "@tanstack/react-query"
{{#if pagesLayoutOverrides}}
import Link from "next/link"
{{#if hasBetterAuthUi}}
import { useRouter{{#if hasAiChat}}, usePathname{{/if}} } from "next/navigation"
{{else}}
{{#if hasAiChat}}
Expand All @@ -17,7 +15,6 @@ import { usePathname } from "next/navigation"
{{/if}}
import { getOrCreateQueryClient } from "{{alias}}lib/query-client"

{{#if pagesLayoutOverrides}}
function getBaseURL() {
if (typeof window !== "undefined") {
return window.location.origin
Expand All @@ -29,14 +26,13 @@ function getBaseURL() {

return "http://localhost:3000"
}
{{/if}}

export default function BtstPagesLayout({
children,
}: {
children: React.ReactNode
}) {
{{#if pagesLayoutOverrides}}
{{#if hasBetterAuthUi}}
const router = useRouter()
{{/if}}
const queryClient = getOrCreateQueryClient()
Expand All @@ -45,17 +41,21 @@ export default function BtstPagesLayout({
const pathname = usePathname()
const showChatWidget = !pathname.startsWith("/pages/chat")
{{/if}}
{{#if pagesLayoutOverrides}}
const baseURL = getBaseURL()

return (
<QueryClientProvider client={queryClient}>
<StackProvider
basePath="/pages"
router={nextRouter()}
api={{{providerApiLiteral}}}
{{#if pagesLayoutOverrides}}
overrides={
{
{{{pagesLayoutOverrides}}}
}
}
{{/if}}
>
{{#if hasAiChat}}
{!hasApiKey && (
Expand Down Expand Up @@ -83,7 +83,4 @@ export default function BtstPagesLayout({
</StackProvider>
</QueryClientProvider>
)
{{else}}
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
{{/if}}
}
12 changes: 4 additions & 8 deletions packages/cli/src/templates/nextjs/preview-client.tsx.hbs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"use client"

import { useState } from "react"
import { useRouter } from "next/navigation"
import Link from "next/link"
import { QueryClientProvider } from "@tanstack/react-query"
import { StackProvider } from "@btst/stack/context"
import { nextRouter } from "@btst/stack/next"
import { getOrCreateQueryClient } from "{{alias}}lib/query-client"
import {
PageRenderer,
Expand All @@ -31,22 +31,18 @@ interface PreviewPageClientProps {
*/
export default function PreviewPageClient({ slug }: PreviewPageClientProps) {
const [queryClient] = useState(() => getOrCreateQueryClient())
const router = useRouter()
const baseURL = getBaseURL()

return (
<QueryClientProvider client={queryClient}>
<StackProvider<PluginOverrides>
basePath="/preview"
router={nextRouter()}
api={{{providerApiLiteral}}}
overrides={
{
{
"ui-builder": {
apiBaseURL: baseURL,
apiBasePath: "/api/data",
componentRegistry: defaultComponentRegistry,
navigate: (path) => router.push(path),
refresh: () => router.refresh(),
Link: ({ href, ...props }) => <Link href={href || "#"} {...props} />,
},
}
}
Expand Down
8 changes: 4 additions & 4 deletions packages/cli/src/templates/nextjs/public-chat-page.tsx.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { ChatLayout } from "@btst/stack/plugins/ai-chat/client"
import { StackProvider } from "@btst/stack/context"
import { nextRouter } from "@btst/stack/next"
import type { AiChatPluginOverrides } from "@btst/stack/plugins/ai-chat/client"
import { QueryClientProvider } from "@tanstack/react-query"
import { getOrCreateQueryClient } from "{{alias}}lib/query-client"
Expand All @@ -27,13 +28,12 @@ export default function PublicChatPage() {
<QueryClientProvider client={queryClient}>
<StackProvider<PluginOverrides>
basePath=""
router={nextRouter()}
api={{{providerApiLiteral}}}
overrides={
{
{
"ai-chat": {
mode: "public",
apiBaseURL: baseURL,
apiBasePath: "/api/data",
navigate: () => {},
},
}
}
Expand Down
19 changes: 4 additions & 15 deletions packages/cli/src/templates/react-router/form-demo-route.tsx.hbs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useState } from "react"
import { Link, useNavigate, useParams } from "react-router"
import { useParams } from "react-router"
import { QueryClientProvider } from "@tanstack/react-query"
import { StackProvider } from "@btst/stack/context"
import { reactRouter } from "@btst/stack/react-router"
import { FormRenderer } from "@btst/stack/plugins/form-builder/client/components"
import type { FormBuilderPluginOverrides } from "@btst/stack/plugins/form-builder/client"
import { getOrCreateQueryClient } from "{{alias}}lib/query-client"
Expand All @@ -25,27 +26,15 @@ type PluginOverrides = {
*/
export default function FormDemoPage() {
const { slug } = useParams()
const navigate = useNavigate()
const [queryClient] = useState(() => getOrCreateQueryClient())
const baseURL = getBaseURL()

return (
<QueryClientProvider client={queryClient}>
<StackProvider<PluginOverrides>
basePath=""
overrides={
{
"form-builder": {
apiBaseURL: baseURL,
apiBasePath: "/api/data",
navigate: (path) => navigate(path),
refresh: () => window.location.reload(),
Link: ({ href, to, ...props }) => (
<Link to={href || to || "#"} {...props} />
),
},
}
}
router={reactRouter()}
api={{{providerApiLiteral}}}
>
<main className="container mx-auto px-4 py-8">
<div className="max-w-2xl mx-auto">
Expand Down
Loading
Loading