<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[[nodebb-plugin-internalnotes] internal notes and topic assignment]]></title><description><![CDATA[<p dir="auto">Disclaimer | 100% transparency<br />
This plugin is vibe coded! So if there are issues that should be resolved, please let me know.<br />
I tried to ensure that the AI followed to the guidelines and made it write a report in <a href="https://github.com/BrutalBirdie/nodebb-plugin-internalnotes/blob/master/NODEBB_STANDARDS_AUDIT.md" rel="nofollow ugc">NODEBB_STANDARDS_AUDIT.md</a></p>
<hr />
<h2><a href="https://github.com/BrutalBirdie/nodebb-plugin-internalnotes/actions/workflows/publish-npm.yml" rel="nofollow ugc"><img src="https://camo.nodebb.org/0d426960f3428f7b563b8816e24e7a689ff8f95c?url=https%3A%2F%2Fgithub.com%2FBrutalBirdie%2Fnodebb-plugin-internalnotes%2Factions%2Fworkflows%2Fpublish-npm.yml%2Fbadge.svg" alt="Lint and publish to npm" class=" img-fluid img-markdown" /></a> <a href="https://github.com/BrutalBirdie/nodebb-plugin-internalnotes/actions/workflows/draft-release.yml" rel="nofollow ugc"><img src="https://camo.nodebb.org/efe8683f72bad79e1443c62abd55996ace1303be?url=https%3A%2F%2Fgithub.com%2FBrutalBirdie%2Fnodebb-plugin-internalnotes%2Factions%2Fworkflows%2Fdraft-release.yml%2Fbadge.svg" alt="Draft release on tag" class=" img-fluid img-markdown" /></a></h2>
<h1>nodebb-plugin-internalnotes</h1>
<p dir="auto">A NodeBB plugin that adds <strong>internal staff notes</strong> and <strong>topic assignment</strong> to forum topics. By default only administrators can see and manage notes and assignments; you can optionally allow global moderators and/or category moderators in the plugin settings. They are completely invisible to everyone else.</p>
<p dir="auto"><strong>Version:</strong> 1.1.0 · <strong>NodeBB:</strong> 4.x (tested on 4.8.1)</p>
<h2>Features</h2>
<ul>
<li><strong>Internal Notes</strong> — Add, view, and delete private notes on any topic. Notes are stored per-topic and include the author and timestamp.</li>
<li><strong>Topic Assignment (User or Group)</strong> — Assign a topic to a specific user or an entire group. All members of an assigned group receive a notification.</li>
<li><strong>"Assign to myself"</strong> — The first option in the assignment modal lets the current user instantly assign the topic to themselves.</li>
<li><strong>Permission-based visibility</strong> — Notes, assignment badges, and the thread tool buttons are completely invisible to regular users. By default only admins can see them; you can enable global moderators and/or category moderators in the plugin settings. No DOM elements are rendered for unprivileged users.</li>
<li><strong>Right sidebar placement</strong> — On topic pages, "Internal Notes" and "Assign Topic" buttons are shown in the far-right sidebar (<code>component="sidebar/right"</code>). A widget is also available for themes that use a different layout.</li>
<li><strong>Admin settings page</strong> — Configure who can access notes: allow global moderators and/or category moderators (ACP &gt; Plugins &gt; Internal Notes &amp; Assignments).</li>
</ul>
<h2>Demo - Version 1.0.2</h2>
<p dir="auto"><img src="https://camo.nodebb.org/aac6a1c019ef363cbce44c84e0e4d483c60bc289?url=https%3A%2F%2Fgithub.com%2FBrutalBirdie%2Fnodebb-plugin-internalnotes%2Fblob%2Fmaster%2F.github%2Fdemo-data%2Fnodebb-plugin-internalnotes-102-demo.webp%3Fraw%3Dtrue" alt="View Demo" class=" img-fluid img-markdown" /></p>
<h2>Installation</h2>
<pre><code class="language-bash">cd /path/to/nodebb
npm install nodebb-plugin-internalnotes
</code></pre>
<p dir="auto">Then activate the plugin from the <strong>Admin Control Panel &gt; Extend &gt; Plugins</strong>.</p>
<p dir="auto"><strong>Where the buttons appear:</strong> On topic pages, the <strong>Internal Notes</strong> and <strong>Assign Topic</strong> buttons are automatically placed in the far-right sidebar (the thin vertical bar on the right edge of the page). No widget setup is required. If your theme does not have this component, you can add the <strong>Internal Notes &amp; Assign Topic</strong> widget to the Global Sidebar in <strong>ACP &gt; Appearance &gt; Widgets</strong> as a fallback.</p>
<h2>Configuration</h2>
<p dir="auto">Navigate to <strong>ACP &gt; Plugins &gt; Internal Notes &amp; Assignments</strong> to configure:</p>
<ul>
<li><strong>Allow global moderators</strong> — Enable to let global moderators view and manage internal notes and assignments (default: off; only admins have access).</li>
<li><strong>Allow category moderators</strong> — Enable to let category moderators view and manage internal notes in their categories (default: off).</li>
</ul>
<h2>Usage</h2>
<ol>
<li>Navigate to any topic as a user who has access (admin, or global/category moderator if enabled in settings).</li>
<li>In the <strong>far-right sidebar</strong> (the vertical bar on the right edge of the page), click <strong>Internal Notes</strong> to open the notes side panel, or <strong>Assign Topic</strong> to assign the topic.</li>
</ol>
<h3>Notes panel</h3>
<ul>
<li>View all existing notes for the topic</li>
<li>Add new notes (supports Ctrl+Enter to submit)</li>
<li>Delete notes</li>
<li>See current assignee (user or group) and unassign</li>
</ul>
<h3>Assignment modal</h3>
<ul>
<li><strong>Assign to myself</strong> — one-click self-assignment (first option)</li>
<li><strong>User tab</strong> — search and select any user by username</li>
<li><strong>Group tab</strong> — search and select any group by name</li>
</ul>
<h2>Compatibility</h2>
<p dir="auto">NodeBB v4.x. Tested on NodeBB 4.8.1.</p>
<p dir="auto">The right-sidebar button placement (injection into <code>component="sidebar/right"</code>) is only tested with the default theme <strong>nodebb-theme-harmony</strong> <a href="https://github.com/NodeBB/nodebb-theme-harmony/tree/v2.1.36" rel="nofollow ugc">v2.1.36</a>. Other themes may need the <strong>Internal Notes &amp; Assign Topic</strong> widget in ACP &gt; Appearance &gt; Widgets.</p>
<h2>For developers</h2>
<ul>
<li><strong><a href="https://github.com/BrutalBirdie/nodebb-plugin-internalnotes/blob/master/DEVELOPMENT.md" rel="nofollow ugc">DEVELOPMENT.md</a></strong> — Local setup, linting, and publishing to npm.</li>
<li><strong><a href="https://github.com/BrutalBirdie/nodebb-plugin-internalnotes/blob/master/TECHNICAL.md" rel="nofollow ugc">TECHNICAL.md</a></strong> — Database keys and API endpoints.</li>
</ul>
<h2>License</h2>
<p dir="auto">MIT</p>
]]></description><link>https://postcall.pub/topic/6c4630df-fd68-4ad5-ada7-870e1484665a/nodebb-plugin-internalnotes-internal-notes-and-topic-assignment</link><generator>RSS for Node</generator><lastBuildDate>Thu, 25 Jun 2026 04:22:50 GMT</lastBuildDate><atom:link href="https://postcall.pub/topic/6c4630df-fd68-4ad5-ada7-870e1484665a.rss" rel="self" type="application/rss+xml"/><pubDate>Thu, 23 Apr 2026 07:53:36 GMT</pubDate><ttl>60</ttl><item><title><![CDATA[Reply to [nodebb-plugin-internalnotes] internal notes and topic assignment on Tue, 02 Jun 2026 17:32:33 GMT]]></title><description><![CDATA[<p dir="auto"><a href="/user/brutalbirdie%40community.nodebb.org">@BrutalBirdie</a> appreciate that. Most of these gotchas and footguns I've actually run into personally as well.</p>
<p dir="auto">Much of it is just known to <a href="/user/baris%40community.nodebb.org">@baris</a> and I, and we don't realize what is missing from those docs that should be explicitly spelled out.</p>
]]></description><link>https://postcall.pub/post/https://community.nodebb.org/post/107297</link><guid isPermaLink="true">https://postcall.pub/post/https://community.nodebb.org/post/107297</guid><dc:creator><![CDATA[julian@community.nodebb.org]]></dc:creator><pubDate>Tue, 02 Jun 2026 17:32:33 GMT</pubDate></item><item><title><![CDATA[Reply to [nodebb-plugin-internalnotes] internal notes and topic assignment on Tue, 02 Jun 2026 17:24:38 GMT]]></title><description><![CDATA[<p dir="auto">As you wish <a href="/user/julian%40community.nodebb.org">@julian</a> I asked Claude for feedback about the docs of NodeBB based on the two plugins I devloped with it.<br />
Here is the output.<br />
Again, 100% AI generated, I take no responsibility for accuracy.</p>
<hr />
<h1>NodeBB Documentation Feedback</h1>
<p dir="auto">Feedback on <a href="https://docs.nodebb.org/development/" rel="nofollow ugc">docs.nodebb.org/development</a>, grounded in building two real NodeBB v4 plugins:</p>
<ul>
<li><strong>nodebb-plugin-internalnotes</strong> — staff notes + topic assignment (custom API routes, widgets, notifications, custom page, ACP settings, privilege checks, DB access).</li>
<li><strong>nodebb-plugin-username-denylist</strong> — registration/username filtering (multiple <code>filter:*</code> hooks, ACP settings).</li>
</ul>
<p dir="auto">The docs were good enough to get a plugin <em>structured</em> correctly (the <code>plugin.json</code> keys, the directory layout, the hook list). Almost all of the friction came <strong>one layer deeper</strong>: once you have a hook firing or a route registered, <em>what is actually in the payload, and which core helper do I call?</em> That is consistently undocumented and had to be reverse-engineered from core source. The notes below are ordered roughly by how much time each cost.</p>
<hr />
<h2>1. Plugin API routes: the base-path / router-scoping story is the single biggest gap</h2>
<p dir="auto">This cost more time than everything else combined.</p>
<p dir="auto">When you register API routes via <code>static:api.routes</code> and <code>routeHelpers.setupApiRoute(router, 'get', '/internalnotes/:tid', ...)</code>, <strong>it is not documented what the resulting public URL is.</strong> Is it <code>/internalnotes/:tid</code>? <code>/api/v3/plugins/internalnotes/:tid</code>? Is the <code>router</code> I receive already scoped to my plugin, or is it the global API router? The client has to call <code>api.get(...)</code> with the <em>correct</em> prefix, and there is no authoritative statement of what that prefix is.</p>
<p dir="auto">We ended up writing a full paragraph of hedging in our own audit doc because we genuinely could not tell from the docs:</p>
<p dir="auto">&gt; <em>"In NodeBB 3, plugin API routes are typically mounted under <code>/api/v3/plugins/</code>; the router passed to <code>static:api.routes</code> may be scoped per plugin… Ensure the router you receive is the plugin-scoped one…"</em></p>
<p dir="auto"><strong>What's missing:</strong></p>
<ul>
<li>A dedicated page: <strong>"Adding REST API routes to a plugin."</strong> Document <code>static:api.routes</code>, the shape of <code>{ router, middleware, helpers }</code>, and the <strong>exact</strong> resulting public path (with the <code>/api/v3/plugins/...</code> prefix spelled out).</li>
<li>Document <code>routeHelpers.setupApiRoute</code> / <code>setupPageRoute</code> / <code>setupAdminPageRoute</code> signatures: argument order, what middleware array is expected, and what <code>helpers</code> (i.e. <code>formatApiResponse</code>) is for.</li>
<li>State clearly how the <strong>client</strong> should call these routes (<code>api.get/post/put/del</code> from the <code>api</code> client module) and how that maps to the server path.</li>
<li>Mention the <strong>route-ordering gotcha</strong>: <code>/:tid/assign</code> and <code>/assignable-users</code> must be registered before <code>/:tid</code> or Express param-matching swallows them. This is a real footgun for plugin authors building more than one route.</li>
</ul>
<p dir="auto">This one topic deserves its own page with a complete, copy-pasteable round-trip example (server route → client call → response).</p>
<hr />
<h2>2. No API reference for the core modules you load via <code>require.main.require('./src/...')</code></h2>
<p dir="auto">Every non-trivial plugin is built on <code>require.main.require('./src/database')</code>, <code>'./src/user'</code>, <code>'./src/groups'</code>, <code>'./src/topics'</code>, <code>'./src/privileges'</code>, <code>'./src/notifications'</code>, <code>'./src/controllers/helpers'</code>, <code>'./src/routes/helpers'</code>, <code>'./src/pagination'</code>, <code>'./src/translator'</code>, <code>'./src/meta'</code>. These are the actual surface area of plugin development — and there is essentially <strong>no reference documentation for any of them.</strong> You discover method names and signatures by reading core source on GitHub.</p>
<p dir="auto">Concrete methods we had to source-dive to find/confirm:</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Module</th>
<th>Methods we needed</th>
<th>Doc status</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>database</code></td>
<td><code>getObject(s)</code>, <code>setObject</code>, <code>sortedSetAdd</code>, <code>getSortedSetRange</code>, <code>getSortedSetRevRange</code>, <code>getSortedSetRevRangeWithScores</code>, <code>sortedSetCard</code>, <code>incrObjectField</code>, <code>deleteObjectFields</code>, <code>deleteAll</code></td>
<td>undocumented</td>
</tr>
<tr>
<td><code>user</code></td>
<td><code>getUsersFields</code>, <code>getUserFields</code>, <code>getSettings</code>, <code>isAdministrator</code>, <code>isGlobalModerator</code>, <code>isModeratorOfAnyCategory</code>, <code>exists</code></td>
<td>undocumented</td>
</tr>
<tr>
<td><code>groups</code></td>
<td><code>search</code>, <code>getMembers</code>, <code>getUserGroups</code>, <code>getGroupFields</code>, <code>exists</code>, <code>isPrivilegeGroup</code></td>
<td>undocumented</td>
</tr>
<tr>
<td><code>topics</code></td>
<td><code>getTopicsByTids</code>, <code>getTopicFields</code>, <code>getFollowers</code>, <code>follow</code>, <code>calculateTopicIndices</code></td>
<td>undocumented</td>
</tr>
<tr>
<td><code>privileges</code></td>
<td><code>topics.filterTids</code>, <code>categories.getModeratorUids</code></td>
<td>undocumented</td>
</tr>
<tr>
<td><code>notifications</code></td>
<td><code>create</code>, <code>push</code></td>
<td>undocumented</td>
</tr>
<tr>
<td><code>controllers/helpers</code></td>
<td><code>formatApiResponse</code>, <code>buildBreadcrumbs</code></td>
<td>undocumented</td>
</tr>
<tr>
<td><code>pagination</code></td>
<td><code>create</code></td>
<td>undocumented</td>
</tr>
<tr>
<td><code>translator</code></td>
<td><code>translate</code> (callback-style)</td>
<td>undocumented</td>
</tr>
</tbody>
</table>
<p dir="auto"><strong>The <code>database-structure</code> doc describes the on-disk schema, not the <code>db</code> <em>API</em> a plugin should call</strong> — those are different things, and a plugin author needs the latter far more often.</p>
<p dir="auto"><strong>What's missing:</strong> a generated or hand-written <strong>"Core APIs available to plugins"</strong> reference, even a thin one. It doesn't need prose for every method — even a typedoc-style dump of signatures for the ~10 modules above would eliminate the single most common reason to leave the docs and read core source. The uncertainty shows up directly in our code as defensive fallbacks like:</p>
<pre><code class="language-js">const cids = await categories.getAllCidsFromSet
    ? await categories.getAllCidsFromSet()
    : await db.getSortedSetRange('categories:cid', 0, -1);
</code></pre>
<p dir="auto">We literally guarded against not knowing whether a method exists, because there was no reference to check.</p>
<hr />
<h2>3. Hook payloads are listed by name, but not by shape</h2>
<p dir="auto">The hooks page is a good index of <em>which</em> hooks exist, but for a filter the only thing that matters is <strong>what's in the object and what I'm expected to return</strong>, and that's almost never specified.</p>
<p dir="auto">Examples from our plugins where we had to infer the shape by logging:</p>
<ul>
<li><code>filter:topic.get</code> → <code>data.topic</code>, plus <code>data.uid</code>. (We needed <code>data.uid</code>; that it's present at all is undocumented.)</li>
<li><code>filter:topics.get</code> → <code>data.topics</code> (array), <code>data.uid</code>. (Different shape from the singular hook — easy to get wrong.)</li>
<li><code>action:topics.purge</code> → <code>{ topics }</code> array. Each element shape?</li>
<li><code>filter:register.check</code> → <code>data.userData.username</code>.</li>
<li><code>filter:username.check</code> → <code>data.username</code>, and you signal rejection by <strong>setting <code>data.error</code></strong>, not by throwing.</li>
<li><code>filter:user.create</code> → <code>data.user.username</code>, and here you <strong>throw</strong> to reject.</li>
</ul>
<p dir="auto">That last pair is the sharpest edge: <strong>three username hooks, with two different rejection conventions</strong> (throw vs. set <code>data.error</code>) and no documentation telling you which is which. We only got it right by trial and error, and documented it ourselves in the plugin README so the next person doesn't have to. A short "rejection convention" note per hook would have saved hours.</p>
<p dir="auto"><strong>What's missing:</strong> for each documented hook, a minimal payload example (<code>{ topic, uid }</code>), what to return, and — critically — <strong>how to signal an error/rejection</strong> (throw vs. mutate <code>data.error</code>), because it is not uniform across hooks.</p>
<hr />
<h2>4. The client-side module system split (AMD vs ES modules) is confusing and underdocumented</h2>
<p dir="auto">A v4 plugin has <strong>three</strong> client entry points with <strong>two different module systems</strong>, and the docs don't clearly flag this:</p>
<ul>
<li><code>scripts</code> (forum client) → AMD/RequireJS style (<code>define</code>, <code>require</code>).</li>
<li><code>acpScripts</code> + <code>modules</code> (ACP) → <strong>ES modules</strong> (<code>import { save, load } from 'settings'</code>).</li>
</ul>
<p dir="auto">Nowhere is it clearly stated which directories get which module system, whether/how client code is transpiled, or what the supported JS feature level is. The style guide says "ES5 for minification," but the ACP path clearly uses ES module <code>import</code> syntax — so the guidance reads as contradictory. We flagged this uncertainty in our own audit and shipped without being sure whether our <code>async/await</code> + template literals in forum scripts would be transpiled or break on older setups.</p>
<p dir="auto"><strong>What's missing:</strong></p>
<ul>
<li>A clear statement: <em>forum <code>scripts</code> use [X module format and Y JS level]; ACP <code>acpScripts</code>/<code>modules</code> use ES modules.</em></li>
<li>Whether the build step transpiles, and what the minimum supported browser/JS target is.</li>
<li>The <code>modules</code> mapping in <code>plugin.json</code> (<code>"../admin/plugins/foo.js": "./public/lib/admin.js"</code>) is subtle and deserves a worked example — the <code>../admin/...</code> virtual path tripped us up.</li>
</ul>
<hr />
<h2>5. ACP settings: the <code>'on'</code>/<code>'off'</code> string gotcha and the <code>settings</code> client module</h2>
<p dir="auto">The ACP settings flow works but has undocumented sharp edges:</p>
<ul>
<li><code>meta.settings.get('myplugin')</code> returns checkbox values as the <strong>strings <code>'on'</code> / <code>'off'</code></strong>, not booleans. Our code is full of <code>settings.allowGlobalMods === 'on'</code> — which we only learned by inspecting stored values. This is a classic footgun that one sentence in the docs would prevent.</li>
<li>The client <code>settings</code> module (<code>import { save, load } from 'settings'</code>) — its API, and how the ACP form field <code>name</code> attributes map to the keys returned by <code>meta.settings.get</code> server-side — is not documented end to end. The "magic" binding between HTML <code>data-*</code>/<code>name</code> attributes and the saved settings object deserves an explicit page.</li>
</ul>
<p dir="auto"><strong>What's missing:</strong> a complete ACP settings round-trip example: the <code>.tpl</code> form markup → <code>save</code>/<code>load</code> client wiring → <code>meta.settings.get</code> server read, <strong>including the checkbox-is-a-string caveat.</strong></p>
<hr />
<h2>6. Widgets: the <code>widget</code> render payload shape is unspecified</h2>
<p dir="auto">The widgets doc covers registration (<code>filter:widgets.getWidgets</code>) and the render hook well enough, but the <strong>render payload is a grab-bag whose contents aren't documented</strong>, so we wrote defensive code probing multiple locations for the same fact:</p>
<pre><code class="language-js">const templateName =
    (widget.templateData &amp;amp;&amp;amp; widget.templateData.template &amp;amp;&amp;amp; widget.templateData.template.name) ||
    (widget.area &amp;amp;&amp;amp; widget.area.template) || '';
const path = (widget.req &amp;amp;&amp;amp; widget.req.path) ? widget.req.path
    : (widget.area &amp;amp;&amp;amp; widget.area.url) || '';
</code></pre>
<p dir="auto">That fallback chain exists purely because we didn't know <strong>which</strong> of <code>widget.req</code>, <code>widget.area</code>, or <code>widget.templateData</code> is guaranteed, or how to reliably answer the very common question <em>"what page/template is this widget rendering on?"</em>. A documented widget-context object (what's always present: <code>req</code>, <code>area</code>, <code>templateData</code>, <code>data</code>, <code>uid</code>) and a recommended "detect current template/route" pattern would remove all of that guesswork.</p>
<hr />
<h2>7. Reusing core templates and the custom-page recipe</h2>
<p dir="auto">We added a custom <code>/assigned</code> page that reuses the core <code>recent</code> template:</p>
<pre><code class="language-js">res.render('recent', { topics, title, breadcrumbs, pagination });
</code></pre>
<p dir="auto">Getting there required reverse-engineering several things with no doc support:</p>
<ul>
<li>That you <em>can</em> <code>res.render()</code> a core template name from a plugin, and which ones are safe/stable to reuse.</li>
<li>The <strong>exact data contract</strong> a reused template expects (<code>topics</code>, <code>breadcrumbs</code>, <code>pagination</code>).</li>
<li>The helper chain to build that data: <code>helpers.buildBreadcrumbs(...)</code>, <code>pagination.create(page, pageCount)</code>, <code>topics.calculateTopicIndices(topics, start)</code>, <code>user.getSettings(uid).topicsPerPage</code>.</li>
</ul>
<p dir="auto"><strong>What's missing:</strong> a "Build a custom plugin page" recipe covering <code>setupPageRoute</code>, rendering your own template vs. reusing a core one, and the breadcrumb/pagination helpers. This is an extremely common plugin need (any "list view") and currently requires reading core controllers.</p>
<hr />
<h2>8. Notifications: a worked example is missing</h2>
<p dir="auto"><code>notifications.create(...)</code> + <code>notifications.push(notifObj, uids)</code> is a common plugin need and entirely undocumented. Things we had to infer:</p>
<ul>
<li>The required/optional fields: <code>type</code>, <code>bodyShort</code>, <code>nid</code>, <code>from</code>, <code>path</code>, plus arbitrary extras (<code>tid</code>).</li>
<li>That <code>nid</code> must be <strong>unique</strong> and is your dedupe key (we encode IDs into it: <code>internalnotes:note:${tid}:${noteId}</code>).</li>
<li>That <code>bodyShort</code> accepts a <strong>translation key with positional args</strong>: <code>&amp;lsqb;&amp;lsqb;internalnotes:notif-assigned-group, ${title}, ${group}&amp;rsqb;&amp;rsqb;</code>.</li>
<li>That <code>create</code> can return falsy and you must null-check before <code>push</code>.</li>
</ul>
<p dir="auto">A single documented example covering these five points would fully cover the feature.</p>
<hr />
<h2>9. i18n: the <code>translator.translate</code> callback vs. namespaced-key-with-args details</h2>
<p dir="auto">The i18n doc explains the <code>languages/</code> layout and <code>&amp;lsqb;&amp;lsqb;namespace:key&amp;rsqb;&amp;rsqb;</code> syntax, but two practical details bit us:</p>
<ul>
<li><strong>Server/client <code>translator.translate('&amp;lsqb;&amp;lsqb;ns:key&amp;rsqb;&amp;rsqb;', cb)</code> is callback-style</strong>, so we had to <code>new Promise((resolve) =&amp;gt; translator.translate(key, resolve))</code> to use it with <code>async/await</code>. Whether a promise-returning form exists isn't documented.</li>
<li><strong>Positional arguments</strong> in keys (<code>&amp;lsqb;&amp;lsqb;ns:key, arg1, arg2&amp;rsqb;&amp;rsqb;</code>) and how they map to <code>%1</code>/<code>%2</code> in the translation string is underspecified.</li>
</ul>
<hr />
<h2>10. Theme coupling: no documented stable extension point for topic-page UI</h2>
<p dir="auto">To put buttons on the topic page we inject into <code>component="sidebar/right"</code>, which <strong>only works with nodebb-theme-harmony</strong> — we had to ship a widget as a fallback and document the limitation ourselves. There's no documented, theme-agnostic extension point ("add an action to a topic") that works across themes. Either <code>filter:topic.thread_tools</code> (and how harmony renders it) should be documented as the blessed path, or the docs should be honest that cross-theme topic-page UI requires widgets/per-theme work.</p>
<hr />
<h2>11. v3 → v4 migration and the local dev loop</h2>
<ul>
<li><strong>Versioning:</strong> the ACP module-loading pattern changed between v3 and v4 (the <code>modules</code> + ES-module approach is v4-only). <code>nbbpm.compatibility</code> controls listing, but there's no "what changed for plugin authors between v3 and v4" migration page. We had to assert <code>^4.0.0</code> and document <em>why</em> (v3 unsupported) ourselves.</li>
<li><strong>Dev loop:</strong> the quickstart shows scaffolding but not the day-to-day reality: <code>npm link</code>/<code>npm install </code> + <strong><code>./nodebb build</code> + <code>./nodebb restart</code> after nearly every change</strong>, that templates and client scripts require a rebuild, and that ACP changes can need a hard refresh. A short "plugin development workflow" section (and any testing guidance — how to write/run plugin tests) would help a lot.</li>
</ul>
<hr />
<h2>Summary — highest-impact additions</h2>
<ol>
<li><strong>A real "Plugin REST API routes" page</strong> — <code>static:api.routes</code>, <code>setupApiRoute</code>, the exact <code>/api/v3/plugins/...</code> path, client <code>api.*</code> calls, and the route-ordering footgun. <em>(biggest win)</em></li>
<li><strong>A "Core APIs for plugins" reference</strong> — even an auto-generated signature dump for <code>database</code>, <code>user</code>, <code>groups</code>, <code>topics</code>, <code>privileges</code>, <code>notifications</code>, <code>controllers/helpers</code>, <code>routes/helpers</code>. <em>(second biggest)</em></li>
<li><strong>Payload shapes + rejection conventions on every hook</strong> — especially the throw-vs-<code>data.error</code> inconsistency across the three username hooks.</li>
<li><strong>Client module-system clarity</strong> — AMD forum scripts vs. ES-module ACP, transpilation, JS target.</li>
<li><strong>ACP settings round-trip</strong> with the <code>'on'</code>/<code>'off'</code> string caveat.</li>
<li><strong>Recipes</strong>: custom plugin page (reusing core templates + breadcrumb/pagination helpers), notifications, and widget-context detection.</li>
</ol>
<p dir="auto">The recurring theme: <strong>the docs get you to the boundary of core, then stop.</strong> Plugin authors live <em>just across</em> that boundary — in the payloads and the <code>require.main.require('./src/...')</code> modules — and that's exactly where documentation is thinnest. Closing that one gap (items 1–3) would remove the large majority of "go read core source" detours.</p>
]]></description><link>https://postcall.pub/post/https://community.nodebb.org/post/107296</link><guid isPermaLink="true">https://postcall.pub/post/https://community.nodebb.org/post/107296</guid><dc:creator><![CDATA[brutalbirdie@community.nodebb.org]]></dc:creator><pubDate>Tue, 02 Jun 2026 17:24:38 GMT</pubDate></item><item><title><![CDATA[Reply to [nodebb-plugin-internalnotes] internal notes and topic assignment on Tue, 02 Jun 2026 02:31:33 GMT]]></title><description><![CDATA[<p dir="auto"><a href="https://community.nodebb.org/user/brutalbirdie" rel="nofollow ugc">@BrutalBirdie</a> can you ask Claude where our documentation is lacking?</p>
<p dir="auto">I am afraid of the answer <img src="https://postcall.pub/assets/plugins/nodebb-plugin-emoji/emoji/android/1f61d.png?v=af4d895db51" class="not-responsive emoji emoji-android emoji--stuck_out_tongue_closed_eyes" style="height:23px;width:auto;vertical-align:middle" title="😝" alt="😝" /></p>
]]></description><link>https://postcall.pub/post/https://community.nodebb.org/post/107291</link><guid isPermaLink="true">https://postcall.pub/post/https://community.nodebb.org/post/107291</guid><dc:creator><![CDATA[julian@community.nodebb.org]]></dc:creator><pubDate>Tue, 02 Jun 2026 02:31:33 GMT</pubDate></item><item><title><![CDATA[Reply to [nodebb-plugin-internalnotes] internal notes and topic assignment on Fri, 29 May 2026 13:41:09 GMT]]></title><description><![CDATA[<p dir="auto">&gt; <a href="https://community.nodebb.org/user/crazycells" rel="nofollow ugc">@crazycells</a> <a href="https://community.nodebb.org/post/107265" rel="nofollow ugc">said</a>:<br />
&gt;<br />
&gt; just the opposite... very good</p>
<p dir="auto">I tried to ensure that Claude sticks to the guidelines. Thanks for feedback.<br />
That has great value for me, so I know my preflight works correctly.</p>
]]></description><link>https://postcall.pub/post/https://community.nodebb.org/post/107266</link><guid isPermaLink="true">https://postcall.pub/post/https://community.nodebb.org/post/107266</guid><dc:creator><![CDATA[brutalbirdie@community.nodebb.org]]></dc:creator><pubDate>Fri, 29 May 2026 13:41:09 GMT</pubDate></item><item><title><![CDATA[Reply to [nodebb-plugin-internalnotes] internal notes and topic assignment on Fri, 29 May 2026 13:17:54 GMT]]></title><description><![CDATA[<p dir="auto">&gt; <a href="https://community.nodebb.org/user/brutalbirdie" rel="nofollow ugc">@BrutalBirdie</a> <a href="https://community.nodebb.org/post/107264" rel="nofollow ugc">said</a>:<br />
&gt;<br />
&gt; <a href="https://community.nodebb.org/user/crazycells" rel="nofollow ugc">@crazycells</a><br />
&gt; Currently, I am using Claude Code.<br />
&gt; For vibe coding the NodeBB plugin I have created a pre-flight instruction for Claude to ingest the docs <a href="https://docs.nodebb.org/development/" rel="nofollow ugc">https://docs.nodebb.org/development/</a> and other, also ingest known good plugins like <code>nodebb-plugin-question-and-answer</code>, <code>nodebb-theme-harmony</code> and others to have a reference for creating code.<br />
&gt;<br />
&gt; Why are you asking, is the code this bad? &lt;img class="not-responsive emoji" src="<a href="https://community.nodebb.org/assets/plugins/nodebb-plugin-emoji/emoji/android/1f605.png?v=14b798222a5" rel="nofollow ugc">https://community.nodebb.org/assets/plugins/nodebb-plugin-emoji/emoji/android/1f605.png?v=14b798222a5</a>" title="<img src="https://postcall.pub/assets/plugins/nodebb-plugin-emoji/emoji/android/1f605.png?v=af4d895db51" class="not-responsive emoji emoji-android emoji--sweat_smile" style="height:23px;width:auto;vertical-align:middle" title=":sweat_smile:" alt="😅" />" /&gt;</p>
<p dir="auto">nope, just the opposite... very good <img src="https://postcall.pub/assets/plugins/nodebb-plugin-emoji/emoji/android/1f642.png?v=af4d895db51" class="not-responsive emoji emoji-android emoji--slightly_smiling_face" style="height:23px;width:auto;vertical-align:middle" title=":)" alt="🙂" /> I liked it, thanks for the efforts. &lt;img class="not-responsive emoji" src="<a href="https://community.nodebb.org/assets/plugins/nodebb-plugin-emoji/emoji/android/1f44d.png?v=14b798222a5" rel="nofollow ugc">https://community.nodebb.org/assets/plugins/nodebb-plugin-emoji/emoji/android/1f44d.png?v=14b798222a5</a>" title="<img src="https://postcall.pub/assets/plugins/nodebb-plugin-emoji/emoji/android/1f44d.png?v=af4d895db51" class="not-responsive emoji emoji-android emoji--+1" style="height:23px;width:auto;vertical-align:middle" title=":+1:" alt="👍" />" /&gt;</p>
]]></description><link>https://postcall.pub/post/https://community.nodebb.org/post/107265</link><guid isPermaLink="true">https://postcall.pub/post/https://community.nodebb.org/post/107265</guid><dc:creator><![CDATA[crazycells@community.nodebb.org]]></dc:creator><pubDate>Fri, 29 May 2026 13:17:54 GMT</pubDate></item><item><title><![CDATA[Reply to [nodebb-plugin-internalnotes] internal notes and topic assignment on Fri, 29 May 2026 11:07:01 GMT]]></title><description><![CDATA[<p dir="auto"><a href="https://community.nodebb.org/user/crazycells" rel="nofollow ugc">@crazycells</a><br />
Currently, I am using Claude Code.<br />
For vibe coding the NodeBB plugin I have created a pre-flight instruction for Claude to ingest the docs <a href="https://docs.nodebb.org/development/" rel="nofollow ugc">https://docs.nodebb.org/development/</a> and other, also ingest known good plugins like <code>nodebb-plugin-question-and-answer</code>, <code>nodebb-theme-harmony</code> and others to have a reference for creating code.</p>
<p dir="auto">Why are you asking, is the code this bad? &lt;img class="not-responsive emoji" src="<a href="https://community.nodebb.org/assets/plugins/nodebb-plugin-emoji/emoji/android/1f605.png?v=14b798222a5" rel="nofollow ugc">https://community.nodebb.org/assets/plugins/nodebb-plugin-emoji/emoji/android/1f605.png?v=14b798222a5</a>" title="<img src="https://postcall.pub/assets/plugins/nodebb-plugin-emoji/emoji/android/1f605.png?v=af4d895db51" class="not-responsive emoji emoji-android emoji--sweat_smile" style="height:23px;width:auto;vertical-align:middle" title=":sweat_smile:" alt="😅" />" /&gt;</p>
]]></description><link>https://postcall.pub/post/https://community.nodebb.org/post/107264</link><guid isPermaLink="true">https://postcall.pub/post/https://community.nodebb.org/post/107264</guid><dc:creator><![CDATA[brutalbirdie@community.nodebb.org]]></dc:creator><pubDate>Fri, 29 May 2026 11:07:01 GMT</pubDate></item><item><title><![CDATA[Reply to [nodebb-plugin-internalnotes] internal notes and topic assignment on Thu, 28 May 2026 22:49:08 GMT]]></title><description><![CDATA[<p dir="auto"><a href="https://community.nodebb.org/user/brutalbirdie" rel="nofollow ugc">@brutalbirdie</a> what do you use to vibe code? claude code?</p>
]]></description><link>https://postcall.pub/post/https://community.nodebb.org/post/107263</link><guid isPermaLink="true">https://postcall.pub/post/https://community.nodebb.org/post/107263</guid><dc:creator><![CDATA[crazycells@community.nodebb.org]]></dc:creator><pubDate>Thu, 28 May 2026 22:49:08 GMT</pubDate></item><item><title><![CDATA[Reply to [nodebb-plugin-internalnotes] internal notes and topic assignment on Thu, 28 May 2026 10:38:44 GMT]]></title><description><![CDATA[<p dir="auto">Update <a href="https://github.com/BrutalBirdie/nodebb-plugin-internalnotes/releases/tag/1.1.1" rel="nofollow ugc">https://github.com/BrutalBirdie/nodebb-plugin-internalnotes/releases/tag/1.1.1</a></p>
<p dir="auto">Fixed:</p>
<ul>
<li>NodeBB v4.12.0 compatibility — broken avatar images in the Assign Topic modal and assignee badge. NodeBB 4.12.0 now HTML-escapes user.picture server-side (turning / into / etc.), and the plugin was escaping it a second time before injecting it into &lt;img src="..." /&gt;. The double-encoding broke the URL. The picture value is now used as-is when interpolated into attribute values.</li>
</ul>
]]></description><link>https://postcall.pub/post/https://community.nodebb.org/post/107259</link><guid isPermaLink="true">https://postcall.pub/post/https://community.nodebb.org/post/107259</guid><dc:creator><![CDATA[brutalbirdie@community.nodebb.org]]></dc:creator><pubDate>Thu, 28 May 2026 10:38:44 GMT</pubDate></item><item><title><![CDATA[Reply to [nodebb-plugin-internalnotes] internal notes and topic assignment on Thu, 23 Apr 2026 19:52:08 GMT]]></title><description><![CDATA[<p dir="auto"><a href="/user/downpw%40community.nodebb.org">@DownPW</a> lookging forward to get feedback <img src="https://postcall.pub/assets/plugins/nodebb-plugin-emoji/emoji/android/1f642.png?v=af4d895db51" class="not-responsive emoji emoji-android emoji--slightly_smiling_face" style="height:23px;width:auto;vertical-align:middle" title=":)" alt="🙂" /></p>
]]></description><link>https://postcall.pub/post/https://community.nodebb.org/post/107045</link><guid isPermaLink="true">https://postcall.pub/post/https://community.nodebb.org/post/107045</guid><dc:creator><![CDATA[brutalbirdie@community.nodebb.org]]></dc:creator><pubDate>Thu, 23 Apr 2026 19:52:08 GMT</pubDate></item><item><title><![CDATA[Reply to [nodebb-plugin-internalnotes] internal notes and topic assignment on Thu, 23 Apr 2026 14:40:12 GMT]]></title><description><![CDATA[<p dir="auto">Cool plugin <img src="https://postcall.pub/assets/plugins/nodebb-plugin-emoji/emoji/android/1f642.png?v=af4d895db51" class="not-responsive emoji emoji-android emoji--slightly_smiling_face" style="height:23px;width:auto;vertical-align:middle" title=":)" alt="🙂" /><br />
test it in dev infra ASAP</p>
]]></description><link>https://postcall.pub/post/https://community.nodebb.org/post/107043</link><guid isPermaLink="true">https://postcall.pub/post/https://community.nodebb.org/post/107043</guid><dc:creator><![CDATA[downpw@community.nodebb.org]]></dc:creator><pubDate>Thu, 23 Apr 2026 14:40:12 GMT</pubDate></item></channel></rss>