PHPackages                             tomvondracek/bolt-discussion - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. tomvondracek/bolt-discussion

ActiveBolt-extension[Utility &amp; Helpers](/categories/utility)

tomvondracek/bolt-discussion
============================

📦 Pluggable, themeable discussion / comments module for Bolt CMS 6 (replies, reactions, async submit, REST polling).

v2.0(1mo ago)17↓50%MITPHPPHP &gt;=8.2CI passing

Since Jun 23Pushed 1mo agoCompare

[ Source](https://github.com/Vondry/bolt-discussion)[ Packagist](https://packagist.org/packages/tomvondracek/bolt-discussion)[ RSS](/packages/tomvondracek-bolt-discussion/feed)WikiDiscussions main Synced 2w ago

READMEChangelog (2)Dependencies (16)Versions (4)Used By (0)

Bolt Discussion
===============

[](#bolt-discussion)

A pluggable, themeable discussion / comments module for **Bolt CMS 6**.

- Anyone (logged-in or anonymous) can post comments
- One-level threaded replies (no deep nesting)
- Emoji reactions (configurable set, deduped per visitor)
- PHP-rendered initial page to avoid layout shift, then async submit and REST polling for live updates
- Editor-level Bolt users (`ROLE_EDITOR`) can delete from the frontend and moderate in the Bolt admin
- Decoupled from contenttypes: a discussion is identified by an arbitrary string `reference`, so you can place many independent discussions anywhere
- Fully themeable per project via `--bd-*` CSS custom properties
- Honeypot + optional spam regex + IP rate-limiting; `auto` or `queue` moderation

Requirements
------------

[](#requirements)

- Bolt CMS `^6.0`, PHP `>=8.2`
- Node `>=22.13` (to build the frontend assets with Symfony Encore)

Installation
------------

[](#installation)

1. **Require the package** (published, or as a local Composer `path` repository):

    ```
    composer require tomvondracek/bolt-discussion
    ```
2. **Build the frontend assets** (shipped source builds to `public/build/`):

    ```
    cd vendor/tomvondracek/bolt-discussion
    npm install && npm run build
    ```
3. **Register the entity mapping.** Extensions aren't bundles, so add this under `doctrine.orm.mappings` in your project's `config/packages/doctrine.yaml`:

    ```
    BoltDiscussion:
      is_bundle: false
      type: attribute
      dir: '%kernel.project_dir%/vendor/tomvondracek/bolt-discussion/src/Entity'
      prefix: 'Bolt\Discussion\Entity'
      alias: BoltDiscussion
    ```
4. **Copy routes/services and create the tables:**

    ```
    bin/console extensions:configure
    bin/console doctrine:migrations:diff        # generates the create-tables migration
    bin/console doctrine:migrations:migrate
    bin/console cache:clear
    ```

Upgrading
---------

[](#upgrading)

When updating to a new version, re-run `doctrine:migrations:diff` / `migrate`in your project so any entity changes are migrated into your database. See [`CHANGELOG.md`](CHANGELOG.md) for per-version upgrade notes (e.g. the `ip_hash`column added in 2.0.0).

Usage
-----

[](#usage)

Place a discussion anywhere in a frontend template. The `reference` is any string that scopes an independent thread:

```
{{ discussion('record-' ~ record.id, { title: 'Comments' }) }}

{# show a count somewhere else #}
{{ discussion_count('record-' ~ record.id) }} comments
```

### Per-instance copy

[](#per-instance-copy)

The second argument is an options map, so each discussion can carry its own composer text (handy when one page hosts several). Each option falls back to the translated default, and you translate it yourself — pass a literal, or your own `|trans` call:

```
{{ discussion('event-2027', {
    title: 'Lineup ideas',
    namePlaceholder: 'name.placeholder'|trans,
    commentPlaceholder: 'What should we book?',
    replyPlaceholder: 'Write a reply…',
    submitLabel: 'Send it'
}) }}
```

OptionOverrides`title`Heading above the thread`namePlaceholder`The name field placeholder`commentPlaceholder`The comment box placeholder`replyPlaceholder`The reply box placeholder`submitLabel`The post-comment button labelThe extension injects its CSS/JS only on pages that actually contain a mount. The initial comment page and composer are rendered during the PHP request, so the widget has its final layout before JavaScript runs. JavaScript hydrates that markup and is then used only for interactions, older pages, and live polling.

Configuration
-------------

[](#configuration)

Edit `config/extensions/bolt-discussion.yaml` (created on first run):

KeyDefaultDescription`moderation``auto``auto` publishes immediately; `queue` holds anonymous comments as *pending* (logged-in users always auto-publish)`poll_interval``10000`Polling interval in ms; `0` disables polling`page_size``10`Root comments per page (newest first); "Load more" reveals older ones. Clamped to 1..100`reactions_enabled``true`Toggle reactions`reactions``['👍','❤️','😂','🎉','😮']`Allowed reaction emoji`replies_enabled``true`Toggle one-level replies`max_length``2000`Max comment length`require_name``true`Require a name from anonymous posters`spam_regex``''`If set, matching bodies are stored as spam`rate_limit_seconds``10`Min seconds between anonymous posts from one IP; `0` disables (logged-in users are never throttled)`reaction_rate_limit``20`Max new reactions per IP per window; `0` disables (logged-in users are never throttled)`reaction_rate_limit_seconds``60`Length of the reaction rate-limit window, in seconds> **Behind a proxy or CDN?** The per-IP limits (`rate_limit_seconds`, `reaction_rate_limit`) and reaction de-duplication only work if Symfony sees the real client IP. Configure [`framework.trusted_proxies`](https://symfony.com/doc/current/deployment/proxies.html)for your environment — otherwise every visitor appears to share the proxy's IP (over-throttling) or a spoofed `X-Forwarded-For` could bypass the limits.

Theming
-------

[](#theming)

The widget is designed to be re-skinned without touching the extension. **Every**visual value resolves to a `--bd-*` custom property declared on `.bolt-discussion`, covering colour, type, radii, spacing, shadow, the per-author avatars, and the reply-thread connector. There are two ways to customize:

**1. Re-declare variables** (the usual path) — map them onto your design tokens:

```
.bolt-discussion.bolt-discussion {
    /* colour */
    --bd-color-accent: var(--color-brand);
    --bd-color-surface: var(--color-white);
    --bd-color-border: var(--color-gray-200);
    /* type & shape */
    --bd-font: var(--font-body);
    --bd-radius: 12px;
    --bd-radius-pill: 999px;
    /* avatars (hue is set per-author; tune saturation/lightness/shape) */
    --bd-avatar-radius: 50%;
    --bd-avatar-sat: 58%;
    --bd-avatar-light: 48%;
    /* reply connector */
    --bd-thread-line: var(--color-gray-200);
}
```

(The doubled `.bolt-discussion` selector raises specificity so your overrides win regardless of stylesheet load order.)

**2. Override BEM classes** for structural or typographic flourishes, e.g.:

```
.bolt-discussion .bolt-discussion__author { text-transform: uppercase; }
.bolt-discussion .bolt-discussion__avatar { box-shadow: 0 0 0 2px var(--color-brand); }
```

See the rockfest theme (`public/theme/rockfest/css/discussion-theme.css`) for a complete "gig-poster" example combining both. You can also override `templates/mount.html.twig` by placing a same-named template in your active theme.

### Built-in UX

[](#built-in-ux)

Modern thread UI out of the box: top composer with an auto-growing field, colour-coded initial avatars, locale-aware relative timestamps (`Intl.RelativeTimeFormat`), reaction chips with a `＋` emoji picker, and collapsible reply threads with a connector line. Respects `prefers-reduced-motion` and ships visible keyboard focus.

### Returning visitors

[](#returning-visitors)

Anonymous visitors are remembered across visits without any login:

- **Name** — the last name posted is stored in `localStorage` (`bolt-discussion:name`) and prefilled into the composer and reply forms, so people don't retype it.
- **Reaction state** — whether you already reacted to a comment ("mine") is tracked per visitor. The identity lives in a year-long `bd_visitor` cookie *and* a `localStorage` id (`bolt-discussion:vid`) sent as an `X-BD-Visitor` header, so it survives even if cookies are cleared. Anonymous ids are validated and namespaced, so they can never impersonate a logged-in user's reactions.

All of this is per-browser and needs no personal data or account.

Admin
-----

[](#admin)

A **Discussion** entry appears in the Bolt admin sidebar (`/bolt/extension/discussion`), listing discussions grouped by reference with per-thread approve / delete / mark-spam.

Translations
------------

[](#translations)

All user-facing text (frontend widget, validation/API errors, and the admin screens) is translated via the `bolt_discussion` translation domain. Catalogs ship for **English, Czech, German, Polish and Dutch** in `translations/`(`bolt_discussion..yaml`), using the English source string as the key.

`extensions:configure` registers the path automatically (it copies `framework.translator.paths` into `config/packages/extension_bolt-discussion.yaml`). Frontend JS strings are translated server-side and passed to the widget via a `data-i18n` attribute. To add a locale, drop a `bolt_discussion..yaml`file in `translations/` and clear the cache.

###  Health Score

40

—

FairBetter than 86% of packages

Maintenance92

Actively maintained with recent releases

Popularity7

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity48

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~3 days

Total

3

Last Release

38d ago

Major Versions

v1.1 → v2.02026-06-30

### Community

Maintainers

![](https://www.gravatar.com/avatar/edbff834c33a0d04959f47a9439ed6a3dfb84acd4a6363e7b57910b4182d85e5?d=identicon)[Vondry](/maintainers/Vondry)

---

Top Contributors

[![Vondry](https://avatars.githubusercontent.com/u/29740752?v=4)](https://github.com/Vondry "Vondry (24 commits)")

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan, Rector

Code StyleECS

Type Coverage Yes

### Embed Badge

![Health badge](/badges/tomvondracek-bolt-discussion/health.svg)

```
[![Health](https://phpackages.com/badges/tomvondracek-bolt-discussion/health.svg)](https://phpackages.com/packages/tomvondracek-bolt-discussion)
```

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
