> ## Documentation Index
> Fetch the complete documentation index at: https://docs.commentify.pro/llms.txt
> Use this file to discover all available pages before exploring further.

# Bring your own markup

> Headless Commentify: CommentifyProvider, useCommentThread, and CommentifyClient with none of the bundled UI.

`<CommentThread />` is the batteries-included path (same Tailwind classes as Blade). When you want the data and none of the opinions, drop it and use the hook. That store is deliberately independent of the built-in components' internal context.

## React

```tsx theme={null}
import { CommentifyClient } from "@commentify/core";
import { CommentifyProvider, useCommentThread } from "@commentify/react";

const client = new CommentifyClient();

function Thread({ id }: { id: number }) {
  const { comments, status, hasMore, post, toggleLike, loadMore } =
    useCommentThread("articles", id, { sort: "newest" });

  if (status === "loading") return <p>Loading…</p>;

  return (
    <>
      {comments.map((comment) => (
        <article key={comment.id}>
          <strong>{comment.user?.name ?? "Guest"}</strong>
          <div dangerouslySetInnerHTML={{ __html: comment.html }} />
          {comment.pending && <em>Awaiting moderation</em>}
          <button onClick={() => void toggleLike(comment.id)}>
            {comment.liked ? "Unlike" : "Like"} ({comment.likes_count})
          </button>
        </article>
      ))}

      {hasMore && <button onClick={() => void loadMore()}>Load more</button>}
      <button onClick={() => void post("Hello")}>Post</button>
    </>
  );
}

export default function Comments({ id }: { id: number }) {
  return (
    <CommentifyProvider client={client}>
      <Thread id={id} />
    </CommentifyProvider>
  );
}
```

Wrap the app in `CommentifyProvider` once so every thread shares one client.

`comment.html` is already markdown + mention links, sanitized the same way Blade renders. Do not run a second markdown pipeline on `body` unless you are building an editor.

## Vue

The hook returns `state` as a single ref:

```vue theme={null}
<script setup lang="ts">
import { useCommentThread } from "@commentify/vue";

const props = defineProps<{ id: number }>();
const { state, post, toggleLike, loadMore } = useCommentThread(
  "articles",
  props.id
);
</script>

<template>
  <p v-if="state.status === 'loading'">Loading…</p>

  <article v-for="comment in state.comments" :key="comment.id">
    <strong>{{ comment.user?.name ?? "Guest" }}</strong>
    <div v-html="comment.html" />
    <em v-if="comment.pending">Awaiting moderation</em>
    <button @click="toggleLike(comment.id)">
      {{ comment.liked ? "Unlike" : "Like" }} ({{ comment.likes_count }})
    </button>
  </article>

  <button v-if="state.hasMore" @click="loadMore()">Load more</button>
</template>
```

Share a client with `createCommentify({ client })` on `createApp()`, or `provideCommentifyClient()` in a parent.

## Guest posts from a custom form

```ts theme={null}
await post(body); // authenticated

// logged out, when core.allow_guests and api.guest_comments:
await client.createComment("articles", id, body, undefined, {
  guest_name: name,
  guest_email: email,
});
```

The bundled composer already sends guest fields when `GET ui` says `features.guests` is true.

## Comment shape

Each comment includes `id`, `body`, `html`, `parent_id`, `is_approved`, `user` (`id`, `name`, `avatar`), `likes_count`, `liked`, `reported`, `replies_count`, nested `replies`, `can`, `is_author`, `is_moderator`, `is_guest`, `pinned`, `pinned_at`, `created_at`, `relative_created_at`, `updated_at`. Guests have `user.id === null` and the guest display name.
