Wiki badge and /wiki links in global search results (#14)
Theme half of the cross-repo wiki global search work (plugin half: sow-nodebb-plugin-wiki#12). Search results tagged by the wiki plugin (`isWikiArticle` + `wikiPath`) now render with a gold "Wiki" badge and link to `/wiki/...` instead of `/post/...`: - `templates/partials/quick-search-results.tpl` — navbar quick-search dropdown - `templates/partials/search-results.tpl` — `/search` page, topics view - `templates/partials/posts_list_item.tpl` — `/search` page, posts view (default). Shared with account/group post lists, where the wiki flags are never set and the conditional no-ops. - `scss/westgate/_search.scss` — badge styling from existing `--wg-gold` / ledger-border tokens; imported from `theme.scss` - `languages/en-GB/westgate.json` — `[[westgate:wiki-badge]]` label - Spec updated with the resolved approach (template override, not client-side decoration) Untagged forum results render byte-identical to Harmony stock. Verification: all three overrides compiled and rendered against benchpressjs with mixed wiki/forum fixture rows (wiki row gets `/wiki/...` href + exactly one badge; forum rows keep stock `/post/...` links, `RE:` prefix and main-post icon preserved). New contract test in `tests/wiki-search-badge-contract.test.js`; suite passes except the pre-existing `global-footer-contract.test.js` failure (stale `join-the-team` link assertion from #10, also failing on main — not touched here). End-to-end visual check (badge in the live dropdown + /search) still needs the dev stack with plugin PR #12 merged. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: #14 Co-authored-by: vickydotbat <vickydotbat@tutamail.com> Co-committed-by: vickydotbat <vickydotbat@tutamail.com>
This commit was merged in pull request #14.
This commit is contained in:
@@ -0,0 +1,95 @@
|
|||||||
|
# Wiki Badge in Global Search Results — Theme Design
|
||||||
|
|
||||||
|
Date: 2026-07-02
|
||||||
|
Status: Approved for planning
|
||||||
|
Scope: theme-only slice of the cross-repo "wiki in global search" work.
|
||||||
|
|
||||||
|
Companion spec (drives this): `sow-nodebb-plugin-wiki` →
|
||||||
|
`docs/superpowers/specs/2026-07-02-wiki-global-search-design.md`. The wiki
|
||||||
|
plugin makes wiki articles searchable and tags each wiki result with
|
||||||
|
`isWikiArticle: true` and `wikiPath` (`/wiki/...`). **This spec covers only the
|
||||||
|
rendering of those tagged results in the theme.**
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
The theme currently ships no search-result overrides — global search
|
||||||
|
(quick-search dropdown + `/search` page) renders with stock NodeBB templates.
|
||||||
|
Once the wiki plugin surfaces wiki articles in search, those results need to be
|
||||||
|
visually marked as wiki and to link to `/wiki/...` instead of `/topic/...`.
|
||||||
|
|
||||||
|
## Input contract (from the wiki plugin)
|
||||||
|
|
||||||
|
Each search result object may carry:
|
||||||
|
|
||||||
|
- `isWikiArticle` — `true` for wiki article hits.
|
||||||
|
- `wikiPath` — the `/wiki/...` URL for the article.
|
||||||
|
|
||||||
|
Forum results do not carry these; they render unchanged.
|
||||||
|
|
||||||
|
## Design
|
||||||
|
|
||||||
|
Mark wiki result rows and point them at the wiki URL, in **both** surfaces:
|
||||||
|
|
||||||
|
1. **`/search` page** — the full results list.
|
||||||
|
2. **Quick-search dropdown** — the navbar type-ahead results.
|
||||||
|
|
||||||
|
For each row where `isWikiArticle` is set:
|
||||||
|
|
||||||
|
- show a wiki icon/badge (e.g. a book/wiki icon plus a small "Wiki" label),
|
||||||
|
consistent with existing theme iconography (see `GAME_ICONS.md`);
|
||||||
|
- set the result's link `href` to `wikiPath`.
|
||||||
|
|
||||||
|
Rows stay in the single relevance-ranked list returned by search; no grouping or
|
||||||
|
reordering.
|
||||||
|
|
||||||
|
### Approach — decide during planning
|
||||||
|
|
||||||
|
Two ways to inject this; pick the smaller one that works against the pinned
|
||||||
|
NodeBB version:
|
||||||
|
|
||||||
|
- **Template override (preferred if a small partial isolates the row):** add a
|
||||||
|
theme override of the NodeBB search result partial(s) — the quick-search
|
||||||
|
results partial and the `/search` results partial — with a conditional badge
|
||||||
|
and `wikiPath` link. Keeps logic declarative and server-rendered.
|
||||||
|
- **Client-side decoration (fallback):** in `public/client.js`, after results
|
||||||
|
render, decorate rows flagged `isWikiArticle` (badge + rewrite href). Smaller
|
||||||
|
surface if the stock partials are awkward to override cleanly, but relies on
|
||||||
|
the data being present in the client payload.
|
||||||
|
|
||||||
|
Planning resolves which, based on the actual stock partials in the pinned
|
||||||
|
NodeBB/theme base.
|
||||||
|
|
||||||
|
**Resolved (2026-07-03): template override.** Harmony's row markup is small and
|
||||||
|
self-contained, so the theme overrides `partials/posts_list_item.tpl` (`/search`
|
||||||
|
posts view — shared with account/groups pages, where the wiki flags are never
|
||||||
|
set and the conditional no-ops), `partials/search-results.tpl` (`/search` topics
|
||||||
|
view), and `partials/quick-search-results.tpl` (dropdown). No client-side
|
||||||
|
decoration needed.
|
||||||
|
|
||||||
|
## Styling
|
||||||
|
|
||||||
|
Add badge styles in the theme SCSS (`scss/`, wired via `theme.scss`), matching
|
||||||
|
existing badge/pill conventions. No new dependencies.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- No search *behavior* changes — indexing, matching, ranking, and result tagging
|
||||||
|
all live in the wiki plugin.
|
||||||
|
- No grouping/separate section — inline badge only.
|
||||||
|
- No changes to non-search surfaces.
|
||||||
|
|
||||||
|
## Testing / verification
|
||||||
|
|
||||||
|
- Visual: run the dev stack (`docker-compose.dev.yml` in `sow-nodebb`), search a
|
||||||
|
term matching a wiki article, confirm the result shows the wiki badge and its
|
||||||
|
link goes to `/wiki/...`; confirm forum results are unchanged. Check both the
|
||||||
|
dropdown and the `/search` page.
|
||||||
|
- Snapshot/DOM assertions only if the theme's existing test setup (`tests/`)
|
||||||
|
makes them cheap; otherwise rely on the visual check (badge/link is not
|
||||||
|
complex logic).
|
||||||
|
|
||||||
|
## Dependencies / sequencing
|
||||||
|
|
||||||
|
Depends on the wiki plugin emitting `isWikiArticle` + `wikiPath` on results.
|
||||||
|
Theme work can be built against a stubbed result but is only end-to-end
|
||||||
|
verifiable once the plugin side lands.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"wiki-badge": "Wiki"
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/* Wiki badge on global search results (quick-search dropdown + /search page).
|
||||||
|
Marks results tagged by nodebb-plugin-westgate-wiki (isWikiArticle/wikiPath). */
|
||||||
|
.westgate-wiki-badge {
|
||||||
|
font-family: var(--wg-font-ui);
|
||||||
|
font-size: 0.62em;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.09em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--wg-gold);
|
||||||
|
background: rgba(194, 163, 90, 0.1);
|
||||||
|
border: 1px solid var(--wg-ledger-border);
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-search-results .westgate-wiki-badge {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<li component="post" class="posts-list-item {{{ if ./deleted }}} deleted{{{ else }}}{{{ if ./topic.deleted }}} deleted{{{ end }}}{{{ end }}}{{{ if ./topic.scheduled }}} scheduled{{{ end }}}" data-pid="{./pid}" data-uid="{./uid}">
|
||||||
|
<hr/>
|
||||||
|
<a class="topic-title fw-semibold fs-5 mb-2 text-reset text-break d-block" href="{{{ if ./isWikiArticle }}}{config.relative_path}{./wikiPath}{{{ else }}}{config.relative_path}/post/{encodeURIComponent(./pid)}{{{ end }}}">
|
||||||
|
{{{ if ./isWikiArticle }}}<span class="badge westgate-wiki-badge"><i class="fa fa-book-open"></i> [[westgate:wiki-badge]]</span> {{{ else }}}{{{ if ./isMainPost }}}<i class="fa fa-book text-muted" title="[[topic:topic]]"></i> {{{ end }}}{{{ end }}}{./topic.title}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="post-body d-flex flex-column gap-1 mb-2">
|
||||||
|
<div class="d-flex gap-2 post-info text-sm align-items-center">
|
||||||
|
<div class="post-author d-flex align-items-center gap-1">
|
||||||
|
<a class="lh-1 text-decoration-none" href="{config.relative_path}/user/{./user.userslug}">{buildAvatar(./user, "16px", true, "not-responsive")}</a>
|
||||||
|
<a class="lh-1 fw-semibold" href="{config.relative_path}/user/{./user.userslug}">{../user.displayname}</a>
|
||||||
|
</div>
|
||||||
|
<span class="timeago text-muted lh-1" title="{./timestampISO}"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div component="post/content" class="content text-sm text-break">
|
||||||
|
{./content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3 d-flex flex-wrap gap-1 w-100">
|
||||||
|
{buildCategoryLabel(./category, "a", "border")}
|
||||||
|
<span data-tid="{./topic.tid}" component="topic/tags" class="lh-1 tag-list d-flex flex-wrap gap-1 {{{ if !./topic.tags.length }}}hidden{{{ end }}}">
|
||||||
|
{{{ each ./topic.tags }}}
|
||||||
|
<a href="{config.relative_path}/tags/{./valueEncoded}"><span class="badge border border-gray-300 fw-normal tag tag-class-{./class}" data-tag="{./value}">{./valueEscaped}</span></a>
|
||||||
|
{{{ end }}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
{{{ if (multiplePages && !config.theme.topMobilebar) }}}
|
||||||
|
<div class="show-more-container">
|
||||||
|
<div class="text-center d-block d-md-none">
|
||||||
|
<a href="{url}" class="btn btn-sm btn-primary">[[search:see-more-results, {matchCount}]]</a>
|
||||||
|
</div>
|
||||||
|
<li role="separator" class="dropdown-divider d-block d-md-none"></li>
|
||||||
|
</div>
|
||||||
|
{{{ end }}}
|
||||||
|
|
||||||
|
<ul id="quick-search-results" class="quick-search-results list-unstyled mb-0 p-0 pe-1 overflow-auto overscroll-behavior-contain ff-base ghost-scrollbar" style="max-width: {dropdown.maxWidth}; max-height: {dropdown.maxHeight};">
|
||||||
|
<li role="separator" class="dropdown-divider d-none d-md-block"></li>
|
||||||
|
{{{ each posts }}}
|
||||||
|
<li data-tid="{posts.topic.tid}" data-pid="{posts.pid}" class="d-flex flex-column gap-1">
|
||||||
|
<a href="{{{ if posts.isWikiArticle }}}{config.relative_path}{posts.wikiPath}{{{ else }}}{config.relative_path}/post/{encodeURIComponent(posts.pid)}{{{ end }}}"
|
||||||
|
class="btn btn-ghost btn-sm ff-secondary rounded-1 text-start text-reset d-block text-truncate px-2 py-1">
|
||||||
|
{{{ if posts.isWikiArticle }}}<span class="badge westgate-wiki-badge"><i class="fa fa-book-open"></i> [[westgate:wiki-badge]]</span> {{{ end }}}<span class="quick-search-title fw-semibold text-sm">{posts.topic.title}</span>
|
||||||
|
<br/>
|
||||||
|
<p class="snippet text-xs text-break text-wrap mb-0">
|
||||||
|
{posts.snippet}
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
<div class="d-flex gap-2 post-info px-2 text-xs">
|
||||||
|
<div class="category-item d-inline-block ff-secondary">
|
||||||
|
{buildCategoryLabel(posts.category, "span")}
|
||||||
|
</div>
|
||||||
|
<span class="timeago text-muted" title="{posts.timestampISO}"></span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{{{ if !@last }}}
|
||||||
|
<li role="separator" class="dropdown-divider"></li>
|
||||||
|
{{{ end }}}
|
||||||
|
{{{ end }}}
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{{{ if multiplePages }}}
|
||||||
|
<div class="text-center mt-2 d-none d-md-block">
|
||||||
|
<a href="{url}" class="btn btn-sm btn-primary">[[search:see-more-results, {matchCount}]]</a>
|
||||||
|
</div>
|
||||||
|
{{{ end }}}
|
||||||
|
{{{ if (multiplePages && config.theme.topMobilebar) }}}
|
||||||
|
<div class="text-center mt-2 d-block d-md-none">
|
||||||
|
<a href="{url}" class="btn btn-sm btn-primary">[[search:see-more-results, {matchCount}]]</a>
|
||||||
|
</div>
|
||||||
|
{{{ end }}}
|
||||||
|
|
||||||
|
{{{if !posts.length}}}
|
||||||
|
<div class="text-center no-results py-2 py-md-0">[[search:no-matches]]</li>
|
||||||
|
<li role="separator" class="dropdown-divider d-block d-md-none"></li>
|
||||||
|
{{{end}}}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
{{{ if matchCount }}}
|
||||||
|
<div class="card card-header text-xs px-2 py-1 fw-semibold border-0 align-self-start">[[search:results-matching, {matchCount}, {search_query}, {time}]] </div>
|
||||||
|
{{{ else }}}
|
||||||
|
{{{ if search_query }}}
|
||||||
|
<div class="badge text-bg-warning align-self-start">[[search:no-matches]]</div>
|
||||||
|
{{{ end }}}
|
||||||
|
{{{ end }}}
|
||||||
|
|
||||||
|
<div id="results" class="search-results" data-search-query="{search_query}">
|
||||||
|
{{{ if showAsPosts }}}
|
||||||
|
{{{ if posts.length }}}
|
||||||
|
<!-- IMPORT partials/posts_list.tpl -->
|
||||||
|
{{{ end }}}
|
||||||
|
{{{ end }}}
|
||||||
|
|
||||||
|
{{{ if showAsTopics }}}
|
||||||
|
{{{ each posts }}}
|
||||||
|
<hr/>
|
||||||
|
<div class="topic-row mb-3">
|
||||||
|
<a class="topic-title fw-semibold fs-5 text-reset text-break" href="{{{ if ./isWikiArticle }}}{config.relative_path}{./wikiPath}{{{ else }}}{config.relative_path}/post/{encodeURIComponent(./pid)}{{{ end }}}">
|
||||||
|
{{{ if ./isWikiArticle }}}<span class="badge westgate-wiki-badge"><i class="fa fa-book-open"></i> [[westgate:wiki-badge]]</span> {{{ end }}}{{{ if !./isMainPost }}}RE: {{{ end }}}{./topic.title}
|
||||||
|
</a>
|
||||||
|
<div class="post-body d-flex flex-column gap-1">
|
||||||
|
<div class="d-flex gap-3 post-info">
|
||||||
|
<div class="post-author d-flex gap-1">
|
||||||
|
<a class="lh-1 text-decoration-none" href="{config.relative_path}/user/{./user.userslug}">{buildAvatar(./user, "16px", true, "not-responsive")}</a>
|
||||||
|
<a class="fw-semibold text-sm" href="{config.relative_path}/user/{./user.userslug}">{../user.displayname}</a>
|
||||||
|
</div>
|
||||||
|
<span class="timeago text-sm text-muted" title="{./timestampISO}"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{{ end }}}
|
||||||
|
{{{ end }}}
|
||||||
|
|
||||||
|
{{{ if users.length }}}
|
||||||
|
<!-- IMPORT partials/users_list.tpl -->
|
||||||
|
{{{ end }}}
|
||||||
|
|
||||||
|
{{{ if tags.length }}}
|
||||||
|
<div class="tag-list row row-cols-2 row-cols-lg-3 row-cols-xl-4 gx-3 gy-2">
|
||||||
|
<!-- IMPORT partials/tags_list.tpl -->
|
||||||
|
</div>
|
||||||
|
{{{ end }}}
|
||||||
|
|
||||||
|
{{{ if categories.length }}}
|
||||||
|
<ul class="categories list-unstyled">
|
||||||
|
{{{each categories}}}
|
||||||
|
<!-- IMPORT partials/categories/item.tpl -->
|
||||||
|
{{{end}}}
|
||||||
|
</ul>
|
||||||
|
{{{ end }}}
|
||||||
|
|
||||||
|
<!-- IMPORT partials/paginator.tpl -->
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Contract: search results tagged by nodebb-plugin-westgate-wiki
|
||||||
|
// (isWikiArticle + wikiPath) render with a wiki badge and link to /wiki/...,
|
||||||
|
// while untagged forum results keep the stock /post/... link.
|
||||||
|
|
||||||
|
const assert = require('assert');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
function read(relativePath) {
|
||||||
|
const filePath = path.join(__dirname, '..', relativePath);
|
||||||
|
assert(fs.existsSync(filePath), `${relativePath} should exist`);
|
||||||
|
return fs.readFileSync(filePath, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertIncludes(haystack, needle, message) {
|
||||||
|
assert(
|
||||||
|
haystack.includes(needle),
|
||||||
|
message || `Expected file to include ${needle}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[
|
||||||
|
['templates/partials/posts_list_item.tpl', './isWikiArticle', '{./wikiPath}', '/post/{encodeURIComponent(./pid)}'],
|
||||||
|
['templates/partials/search-results.tpl', './isWikiArticle', '{./wikiPath}', '/post/{encodeURIComponent(./pid)}'],
|
||||||
|
['templates/partials/quick-search-results.tpl', 'posts.isWikiArticle', '{posts.wikiPath}', '/post/{encodeURIComponent(posts.pid)}'],
|
||||||
|
].forEach(([templatePath, flag, wikiHref, forumHref]) => {
|
||||||
|
const template = read(templatePath);
|
||||||
|
assertIncludes(template, flag, `${templatePath} should branch on the wiki result flag`);
|
||||||
|
assertIncludes(template, wikiHref, `${templatePath} should link wiki results to their wikiPath`);
|
||||||
|
assertIncludes(template, forumHref, `${templatePath} should keep the stock post link for forum results`);
|
||||||
|
assertIncludes(template, 'westgate-wiki-badge', `${templatePath} should render the wiki badge`);
|
||||||
|
});
|
||||||
|
|
||||||
|
assertIncludes(read('theme.scss'), 'westgate/search', 'theme.scss should import the search styles');
|
||||||
|
assertIncludes(read('scss/westgate/_search.scss'), '.westgate-wiki-badge', 'search styles should define the wiki badge');
|
||||||
|
|
||||||
|
const languageKeys = JSON.parse(read('languages/en-GB/westgate.json'));
|
||||||
|
assert(languageKeys['wiki-badge'], 'westgate language file should define the wiki-badge label');
|
||||||
|
|
||||||
|
console.log('wiki search badge contract tests passed');
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
@import "./scss/westgate/categories";
|
@import "./scss/westgate/categories";
|
||||||
@import "./scss/westgate/topics";
|
@import "./scss/westgate/topics";
|
||||||
@import "./scss/westgate/posts";
|
@import "./scss/westgate/posts";
|
||||||
|
@import "./scss/westgate/search";
|
||||||
@import "./scss/westgate/controls";
|
@import "./scss/westgate/controls";
|
||||||
@import "./scss/westgate/widgets";
|
@import "./scss/westgate/widgets";
|
||||||
@import "./scss/westgate/responsive";
|
@import "./scss/westgate/responsive";
|
||||||
|
|||||||
Reference in New Issue
Block a user