Opening a conversation used to mark it read even when the agent never scrolled to the new messages or the tab was in the background. Reply notifications in the bell also stayed unread after the agent read the reply.
Now the browser tells the server which message the agent saw. It only does this when the end of the thread is on screen and the tab has focus. The server moves the agent's last seen time forward to that message and never back.
A reply notification counts as read when the agent's last seen time is at or after the reply. "Mark as unread" marks the old reply notifications read first, so the bell stays the same.
Reply emails and pushes wait a little before sending. They are skipped if the agent already read the message or can no longer see the conversation.
Drop the check that blocked one address on two inboxes and simplify sender
lookup, alias verification and IMAP handling. The inbox form now shows the
connected account first, lines up its fields and shows one hint for unsaved aliases.
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.
Keep local templates unchanged when Meta rejects edits or deletions. Block unsupported template sends and add regression tests for template actions and form controls.
The resolved conversations chart filtered on created_at but grouped by resolved_at, so anything created before the window never showed up, and the tag list dropped tags whose conversations were all older than the window. Dropped the message volume COUNT(DISTINCT) and three of the four users table scans, and added the created_at indexes these queries need. The Pending tile is now labelled Awaiting First Reply since it counts conversations with no agent reply yet.
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.
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.
Resolved the AI embedding conflicts by keeping this branch's generic embedSource path and layering main's tag indexing on top. Search now covers snippets and help articles, tags stay excluded. Dropped SetContentID since main's link-message-media query stamps content_id itself.
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.