An article written in another language can now be joined to an existing
article as its translation, or pulled back out into its own group. Also caps
transcript downloads at 1000 messages and fixes the field widths in the
automation rule and action rows.
Move the translation_group_id migration into v2.9.0 and drop the slug backfill. Existing articles each get their own group and admins link translations from the new UI. The public article page now builds its language links from the group instead of the slug, so translations with different slugs no longer 404.
Expand widget conversations with audience-specific forms, AI handoffs, message previews, and help article translations. Keep existing inbox settings compatible and reject invalid live chat configuration.
Geist replaces Instrument Sans and is served from static/ instead of Google
Fonts, so it works on installs with no internet access.
Public help center pages are cached in Redis via fastcache with ETags, and any
admin write clears the group so edits show up on the next load.
Locales are now a fixed whitelist (internal/helpcenter/locales.go) served via
/api/v1/help-centers/locales, and the admin form uses a combobox instead of a
free-text code input. Validation checks the whitelist instead of a BCP-47 regex.
Search: help_articles gets a generated search_tsv column with per-locale
stemming via a new help_article_search_config() SQL function, weighted
title/excerpt/body (A/B/C), a GIN index, and ts_rank length normalization so
long articles don't win by repetition. Body text is capped at 100K chars to
stay under the 1MB tsvector limit. The trigram ILIKE fallback stays for CJK
and infix matches.
The v2.8.0 migration is consolidated into one-shot CREATE TABLEs matching
schema.sql since the release isn't out yet.
Backend:
- log LinkHelpArticleMedia errors on article create/update
- trim a trailing slash off the root URL in PublicURL
- run uniqueCollectionSlug inside the caller's transaction
- update-article sets author_id directly so clearing the author works
- apply the help_centers template CHECK as a named constraint so it
also lands on installs that ran an earlier build of the migration
Frontend:
- new editor tables get the email-safe inline styles by default
- replace deprecated word-wrap/word-break with overflow-wrap
- reject a default locale that is not in the allowed locales
- show an error when the icon picker sprite fails to load
- name the locale remove buttons and keep classic-only header
fields mounted with v-show
Docs template:
- move the collection link out of <summary>, toggle is its own button
- use logical CSS properties so the sidebar nav renders in RTL
- fix text-rendering keyword casing
public_url is now custom_domain. It only accepts a bare origin, no path or query.
When a help center has a custom domain, its pages live at /{locale} on that domain. Requests on that host fall through to the router's not-found handler, which prepends /hc/{slug} and re-runs the router once, so nothing about the routes changes. All page links come from a new BasePath value, which is empty-prefixed on a custom domain and /hc/{slug} on the app host. Hits on the old /hc/{slug} paths 301 to the custom domain so there is one canonical address per page.
Redirects now send a path-only Location built through fasthttp's URI encoder. ctx.Redirect would turn it into an absolute http:// URL, since nginx forwards to the app over plain HTTP, and a raw path would let /\host in the URL read as another site.
robots.txt on a custom domain also disallows the search pages.
Help centers now have a template setting with two layouts. Classic is
the default: the existing hero banner, large search bar and collection
cards. Docs is new: sticky top bar, collections sidebar on every page,
command palette search (Ctrl+K) with a no-JS fallback form, and an
on-page table of contents.
Backend adds a template column (default 'classic', checked to the two
values), threads it through insert/update queries and the request
validator, and resolves page template names as '<template>-<page>' when
rendering public pages. Docs pages also get the published tree for the
sidebar.
Public templates are restructured so each layout is fully isolated and
a third template is just a new directory plus its CSS file:
web-templates/help/shared/ holds the head, announcement, lang switcher,
icons and shared JS; help/classic/ and help/docs/ each hold their own
layout and five pages. The classic stylesheet is renamed to
help-center-classic.css so the head links help-center-<template>.css
directly. Template parsing now globs the help/ subdirectories too.
The admin create and edit forms get a template picker, and classic-only
styling fields (header background, collection layout, card icons) are
hidden when docs is selected.
A batch of correctness fixes from a code review of the help center branch.
Public pages:
- hide the article author in JSON-LD when the theme hides the byline, so
the author name no longer leaks in structured data.
- make agent and AI assistant avatars public on upload (and backfill AI
assistants in the v2.7.0 migration) so author avatars don't 401 for
anonymous visitors.
- return the same 401 for missing and existing private uploads to an
anonymous caller, so /uploads can't be used to probe which private
media exists.
- switch the bare /hc/{slug} home redirect from 301 to 302 since the
default locale is mutable and a permanent redirect gets cached.
- match the request host against the public URL by hostname, ignoring
any port, so robots.txt and sitemaps resolve on custom domains.
- fix the lazy-load regex so an img with data-loading or alt text like
"loading=x" still gets loading="lazy" and decoding="async".
- stop logging searches shorter than the minimum length, which never
actually run, so they don't pollute the no-result insights.
Admin and data integrity:
- take the help center row lock in UpdateArticle, MoveArticle,
CreateCollection and UpdateCollection, and run the collection depth
check inside that transaction, so concurrent edits can't create
duplicate slugs or overshoot the max depth.
- default the collection locale before validating the parent, so
creating a child collection without a locale no longer 400s.
- keep a hand-edited help center slug when the name is edited again.
- render the Snippets page title in plural.
- release the feedback dedup key when the insert fails, so a retry
isn't swallowed as a duplicate.
Batch of fixes from a review of the help-center branch.
Slugs. A long title made a slug longer than the 200 char cap, so the
save failed with a slug error on a form that has no slug field. The
"-2" dedup suffix could also push a slug past the cap after validation
ran, which locked the article out of every later edit. Slug generation
now truncates to fit, and collections get the same dedup pass articles
already had. Accented titles kept losing letters, so accents now fold
to their ASCII base. Titles in scripts with no ASCII form still fall
back to a random slug.
Locales. Removing a language from a help center stranded its
collections and articles. They had no admin UI path and no public URL,
but stayed in the AI index. Dropping a language that still holds
content is now rejected. Only languages the help center allows today
are checked, so an existing orphan can still be saved.
Theme. The luminance check only parsed 3 and 6 digit hex while the
sanitizer accepts 4 and 8 digit too, so an 8 digit header colour fell
back to dark text on a dark header. The free text colour fields had no
validation, so a bad value was silently thrown away on save while the
toast said it worked. The article editor was missing --hc-accent-ink,
which made button links invisible while editing.
Embeddings. The chunker changed the text it emits but the fingerprint
did not change, so nothing already indexed was ever re-chunked. The
chunker version now feeds the fingerprint. Deleting a collection or a
help center left the cascaded articles' vectors behind until the next
sweep, so those now clean up on delete.
Public API. The public JSON endpoints returned author name and avatar
even when the theme had authors turned off.
Media and search. Anonymous /uploads requests now hit a rate limit
before the DB lookup, and served files carry a one day cache header.
S3 redirects are no-store because a presigned URL expires. Public
search now enforces the same two character floor the typeahead uses,
so a one character query cannot seq-scan every article body.
Each help center gets an optional public_url. When set, canonical links, hreflang
alternates, og:url, the sitemap and robots.txt point at that host instead of the
app root URL. Stored media URLs are rewritten to root-relative paths so logos,
favicons, header images and article images resolve on whichever host serves the
page. The URL is validated on both the form and the backend, and trailing slashes
are trimmed.
Also fixes issues found in code review:
- the help center template used a locale link as the data root inside the
alternates loop, which broke every public page on a help center with two or
more locales
- creating an article posted the clicked collection id, so changing the
collection in the sheet filed the article in the wrong place
- the collection field's error message was hidden when the locale had no
collections, so Create did nothing with no error shown
- switching the header type or unticking popular articles dropped those theme
values on save, because vee-validate unsets fields that unmount
- closing a new collection sheet with Esc left the parent id set, so the next
collection you edited got re-parented
- a table row with th labels in the first column was treated as a header row
when preparing article content for embeddings
- the img tag regex stopped at a > inside an attribute value and injected
loading/decoding in the middle of an attribute
The customize page preview now has a page selector (landing or a sample
article) and a device selector (desktop, tablet, mobile) with the frame
rendered at real device width and scaled to fit, wrapped in a browser
chrome mockup showing the live title, favicon and URL.
Themes gain an announcement banner shown above the header, a popular
articles section toggle with a custom label, and an icon tile option for
collection cards. Theme normalization now starts from defaults instead
of zero values.
Articles get a selectable author (validated as an agent or AI assistant)
separate from the new created_by column that records who created it. A
soft-deleted author no longer blocks saving an existing article.
The assistant test tab looked up every search hit as a snippet, so a help
article showed an unrelated snippet's title or vanished from the list. Also
drops the help center view count, which nothing ever read.
Permissions:
- the admin help center read routes only checked that you were logged in, so
any agent could read draft articles and every help center's custom CSS and
JS. They need help_center:manage now, like the write routes already did.
Article editor:
- adding a YouTube video made the whole page unclickable, because the iframe
swallowed every mouse event. It ignores the pointer while editing now.
- the collapsible body looked editable but was not. A closed <details> is not
clickable, so the editor marks it open instead of just showing the body.
- headings all rendered at the same size. Article styles moved into the shared
article-content.css so the editor and the published page cannot drift.
- there was no way to type below a callout, table or video at the end of an
article. There is always a line after the last block now.
- Enter on the empty last line of a collapsible gets you out of it.
- the toolbar sits above the article instead of floating at the bottom.
- videos can be selected and aligned, and show an outline when selected.
- H1 is gone from the toolbar. The article title is the page's H1.
Public pages:
- the excerpt shows under the article title, and is only what the author
wrote. It used to be auto-filled from the article body.
- the table of contents is a sticky rail beside the article on desktop and
collapses on small screens. It covers H2 to H4.
- breadcrumbs are a real list, so screen readers can count them and the "/"
is not read out.
- sub collection titles on a collection page are headings now. That page had
one heading before.
- added a skip link, nav labels, and a language switcher that no longer
claims to be a menu it never implemented.
- image alt text with a "?" or ":" was being dropped on save.
- a failed search no longer leaves the old suggestions on screen.
- a dark header colour picks a readable text colour on its own.
The search box now shows matching articles as you type, and you can pick
one with the arrow keys. Cards get an icon position setting (beside the
title, above it, or centered) and old search logs are cleaned up daily.
Editing a help center already worked from a sheet on the list page. This
moves it to its own screen so the public home page can render beside the
form as a live preview, built from the unsaved values.
Fixes:
- The preview dropped the entire theme. The form sent the card column
count as a string and the backend could not read the theme JSON, so it
fell back to an empty theme. Header and footer colors, taglines,
footer links and social links all disappeared from the preview.
- An article whose language did not match its collection stayed live on
the public site and in the sitemap, but its breadcrumb pointed at a
page that 404s, and it was not visible in either language's tree. The
public queries now require the article and its collection to share a
language.
- Collections could be created in a language the help center does not
list. That content is unreachable on the site and in the admin tree,
so it is rejected now. An unknown language in the tree URL falls back
to the default instead of showing an empty tree.
- Saving a collection or article in another language looked like nothing
happened, because the tree only shows one language. The tree now
switches to the language you saved into.
- Link fields accepted a value with no scheme. A bare host renders as a
relative link and navigates inside the help center. The form now
validates them and the backend drops bad ones on save.
- Delete buttons in the help center used the primary style. They use the
destructive style now, like the rest of admin.
- Collection cards left the icon out when none was set, so cards in a row
did not line up. The icon slot always renders and the meta row sits at
the bottom of the card.
- The public header stacked into a staircase on phones. It now wraps to
two rows.
- Tree rows were mouse only. The row title is a button now, so it is
reachable by keyboard.
- Sort order updates ran as separate statements and could half apply.
They run in one transaction.
- x-default hreflang was emitted on single language help centers.
Media:
- media rows now take the private flag from the caller instead of
guessing it from the model type
- agent avatars upload as public so help center readers can load them,
contact and visitor avatars stay private
- backfill existing agent avatars to public
Public pages:
- dedupe article feedback by reader IP for 24 hours
- keep the article locale when submitting feedback
- skip view count bumps from crawlers on the JSON API
- only log a search when the search itself worked
Admin and data:
- lock the help center row while creating an article, since slugs are
unique per help center but the DB index is per collection
- block changing a collection language when it still has articles or
sub collections
- validate article locale on every update, not only when the collection
changes
- reject slugs with characters that break /hc/ URLs
- exclude the whole subtree from the parent picker so a collection
cannot be moved into itself
- drop articles of a paused help center from the AI index
- take the embed generation at enqueue time so jobs commit in edit order
Editor and UI:
- show an error when a YouTube URL is not valid
- do not break the icon picker when the sprite fails to load
- ignore a slow article fetch after another article is opened
i18n: move generic strings like Published, Archived, Slug, Icon, Author,
Language, Text color and Platform to globals, and drop hint text that
just repeated the field label.
Public pages were missing most of what a search engine needs. They now serve
a sitemap index at /sitemap.xml plus a robots.txt, and every page carries a
canonical URL, hreflang alternates, an x-default, and JSON-LD. Article pages
also send published and modified times. Search pages and the markdown view
are marked noindex. Requests from bots no longer bump the view counters, so
the insights numbers reflect real readers. Offscreen images in articles are
lazy loaded.
Public page text is now translated per locale instead of always using the
desk language, and pages with more than one translation show a language
switcher.
In the admin, collections and articles can be dragged to reorder, and an
article can be dragged into a different collection. Both save through new
sort order endpoints. Collections can have a lucide icon, picked from a new
icon picker backed by a sprite sheet. The landing page has layout options
for card grid or one per row, cards per row, and which details to show.
The help center list is a data table now, so HelpCenterCard is gone. The
tree page moved into AdminSplitLayout, its actions sit on their own row
below the breadcrumb, and it gained visit site and expand/collapse all.
Review pass over the help center commit. Bug fixes:
- Image-only or video-only articles were rejected on save because the
editor blanked the content when it had no text.
- An article could be moved into a collection in another help center.
Slugs are only unique per help center, so two articles could end up
on the same public URL.
- The article editor fired the "agent is typing" indicator into
whatever conversation was last open, which the contact could see.
- Unsupported locales like /hc/x/zz-ZZ returned an empty 200 page with
a canonical URL, so crawlers saw unlimited URLs. They 404 now.
- Article feedback was accepted for paused help centers.
- French pages announced lang="en".
- Editing a help center popped validation errors mid-keystroke.
Shared code that was duplicated:
- Help articles had their own copy of the snippet embedding lifecycle.
The copy was missing the wait group, the concurrency cap and the
generation gating, so article embeds were killed at shutdown, all hit
the provider at once, and a slow embed could overwrite a newer edit or
bring back a deleted article. Both content types now go through one
embedSource lifecycle. Fingerprints are unchanged, so nothing
re-embeds on deploy.
- Callout and collapsible CSS was written twice, once for the editor and
once for the public page. They now share article-content.css, so the
editor stays WYSIWYG.
- The excerpt was computed three ways. The page uses the stored one.
Less work at runtime:
- The admin tree no longer sends every article's HTML. The edit sheet
loads the one article it needs. Publishing an article used to
re-download the whole help center.
- Embedding reconcile no longer reads every article body every minute.
- The tree view fetched the help center twice on load.
Cleanups: one update-article route instead of two, media privacy comes
from the model type instead of a bool threaded through the handler, a
LinkListField component replaces three copies of the same repeater,
TreeNode renders the article row once, and 12 duplicate or dead i18n
keys are gone.
Agents can now publish a public knowledge base per help center.
Backend:
- new internal/helpcenter package for help centers, collections and articles
- admin JSON API behind a new help_center:manage permission
- public /hc/{slug} pages, JSON API, search and sitemap, all rate limited
- migration v2.7.0 for the new tables
AI:
- published articles with the AI flag on get embedded, so the agent answers
from them. Unpublishing or clearing the flag removes the embeddings.
Media:
- media rows now carry a private flag. Help article images are public so the
public pages can serve them without auth, and only agents with
help_center:manage can upload them.
Frontend:
- admin help center list, tree view and article editor
- the old TextEditor is split into ArticleEditor and ConversationEditor over a
shared useTextEditor composable, so article-only tools like callouts,
collapsibles and YouTube embeds stay out of the reply box