← Iterato Self Helpdesk

Kora Help Center — Product Requirements Document

Updated Oct 7, 2026

Feature: Help Center (Blog-style KB) — Part B Feature 3 Version: 1.0 · Status: Approved for build (plan v3, 2026-10-04) Companion docs: Kora_KB_BRD_v1.0.md · Kora_KB_TechDoc_v1.0.md · docs/kb/discovery.md


1. Concept

Every workspace gets a public, branded, blog-style help center:

  • Listing page — usekora.app/p/helpcenter/<slug>: header (logo/name/intro), search, article cards (cover, title, summary, date) newest-first, optional subscribe box, "Powered by Kora" footer.

  • Article page — usekora.app/p/helpcenter/<slug>/<article-slug>: cover, title, meta line, rich body, 👍/👎, "Last updated", share links.

  • Directory — usekora.app/p/helpcenters: all public help centers.

  • ws-id variant — /p/helpcenter/ws_XXXXXXX/… always resolves (redirects to the primary slug URL).

  • Embed mode — ?embed=1 renders a compact version for the chat-widget iframe.

  • Dashboard — /dashboard/help-center: Articles · Analytics · Subscribers · Settings; Notion-style full-page editor at /dashboard/help-center/:id.

Design language: monochrome black & white by default, typography-first, light/dark/system theme toggle (system is the default on every load until the visitor chooses), workspace branding layered on top when configured.

2. Roles & permissions

Notes: publish charges 1 credit regardless of role. All member routes use the standard Bearer-token realm (resolveWorkspaceMember).

3. Functional requirements

Each FR carries acceptance criteria (AC). "Member" = any workspace role; "author" = Owner/Admin/Manager.

FR-1 Articles — lifecycle

Articles have: title (≤200 chars, required), slug (per-workspace unique, editable), summary (≤280 chars), cover image, meta_title (≤70) / meta_description (≤160), rich body, status draft|published, visibility public|unlisted, is_seo toggle, timestamps, soft delete.

  • AC1 Author creates a draft; it appears under Articles → Drafts; nothing is publicly reachable.

  • AC2 Publishing sets published_at (first time) and charges exactly 1 credit (see FR-11); the article becomes publicly reachable if the center is public.

  • AC3 Unpublishing returns the article to draft; public URLs 404; republishing is free and re-lists it (keeps original published_at if never unpublished-to-draft edits? — simpler: published_at = most recent publish; listed order by it).

  • AC4 Unlisted articles: reachable at their direct URL while published, excluded from listing, directory, search, sitemap feed, and carry noindex.

  • AC5 Delete = soft (Trash filter); public URLs 404; Restore returns previous state.

  • AC6 Slug edits are free and immediate; old slugs do not redirect at article level (article slugs are cheap and internal — document in UI copy: "changing the slug breaks existing links").

FR-2 Editor — Notion style

Full-page editor. Big borderless title input; TipTap canvas with:

  • Blocks via slash menu (/ → Heading 1/2/3, Text, Bullet list, Numbered list, Quote, Code block, Divider, Image, Video, Table).

  • Markdown input rules (type # , - , > , ``` etc. — built into TipTap).

  • Bubble menu on text selection: bold, italic, underline, strike, code, link.

  • Image block: upload (multipart → kb asset) with progress, or paste URL; renders inline.

  • Video block: paste YouTube/Vimeo/Loom URL → responsive 16:9 embed; or upload mp4/webm (existing per-file cap) → inline <video controls>.

  • Right rail: slug, summary, cover upload + crop-free preview, meta fields with pixel-count hints, visibility switch, SEO switch, Publish button (labeled "Publish — 1 credit" when not yet charged; "Publish" free afterwards), Share draft link, Revisions.

  • ⌘S saves; autosave every 30s when dirty; unsaved-changes guard on navigation; word count.

  • AC1 All listed block types insert, render, and round-trip through save/load.

  • AC2 Paste-with-formatting from web/Docs sanitizes to the allowed block set (no unknown node types persist).

  • AC3 Autosave + manual save produce a revision snapshot; reload restores exactly.

  • AC4 Editor never shows raw HTML; no native dialogs/selects anywhere.

FR-3 Slug system (help-center identity)

  • Default identity = ws_… id (works from day zero, costs nothing, never expires).

  • Claims: Owner/Admin claims a custom slug (^[a-z0-9]{3,30}$, case-insensitive input → stored lowercase). Max 5 claims per workspace lifetime; deleting/never refundable. Latest claim = primary; all previous slugs keep resolving via 301 → primary URL.

  • Global uniqueness across workspaces; reserved list blocked (kora, usekora, helpcenter, helpcenters, api, app, www, admin, blog, help, docs, pricing, status, dashboard, embedd, public, files, auth, f, p, ws).

  • AC1 Claim succeeds → /p/helpcenter/<slug> serves the center; ws-id path still works.

  • AC2 Second claim → newest is primary; first slug now 301s to the new primary URL; article URLs likewise.

  • AC3 6th claim rejected with count shown ("5 of 5 used").

  • AC4 Taken/reserved/invalid slugs → clear field-level errors (409/422).

  • AC5 Primary can be switched among already-claimed slugs without spending a claim.

  • AC6 A slug released by no mechanism — claims are permanent (anti-squat).

FR-4 Help-center settings & branding

Fields: is_public switch (default on), title (default = workspace name + " Help Center"), intro line, meta title/description/image for the listing page, accent color, light fg+bg, dark fg+bg, title font + body font (from the existing Google-font list), subscription toggle (default off). Owner/Admin only.

  • AC1 Private center: listing/article/directory/API return 404 publicly (direct link included); dashboard unaffected; directory excludes it.

  • AC2 Branding applies live to public pages; unset fields fall back to monochrome defaults; contrast guard warns when fg/bg are too close.

  • AC3 Fonts lazy-load from the existing font list (idempotent loader).

  • AC4 Settings changes take effect ≤ one page reload; no rebuild.

FR-5 Public listing page

  • Monochrome default: near-black on white / white on near-black; system theme default; visitor toggle persists (localStorage); branding overrides colors/fonts/accent.

  • Header: workspace logo (if any) or monogram, title, intro. Search box. Article cards (cover if set, title, summary, date) newest-first by published_at. Subscribe box if enabled. "Powered by Kora" footer (links to usekora.app) — always present, cannot be disabled in v1.

  • AC1 Renders <1s on 4G for a 20-article center (API payload ≤ ~30KB gzipped, covers lazy-loaded).

  • AC2 Only published + public + not deleted articles appear.

  • AC3 ?embed=1 hides header/footer nav, compacts spacing for the widget iframe.

  • AC4 404 states: unknown slug, private center (indistinguishable from unknown — no existence leak).

FR-6 Public article page

Cover (if set) or typographic header, title, "Last updated ", rich body rendered from the stored TipTap JSON via the allowlist renderer, 👍/👎, share (copy link, X/Twitter, LinkedIn), back-to-center link, "More from " (latest 3 others). Meta: title = meta_title || title; description = meta_description || summary; og:image = cover; canonical = /p/helpcenter/<primary-slug>/<article-slug>; noindex unless (article is_seo AND center public AND article public+published).

  • AC1 Body blocks render 1:1 with editor output; links open rel="noopener noreferrer" in new tab; embeds are responsive and domain-allowlisted.

  • AC2 Unlisted article loads at direct URL with noindex + not in "More from".

  • AC3 Deleted/unpublished → 404.

FR-7 Search (public + dashboard)

FTS5 over (title, summary, content_text); public search restricted to published + public + listed articles of that center; dashboard search over all non-deleted articles of the workspace.

  • AC1 Multi-word queries match; results ranked (title hits > summary > body).

  • AC2 Zero-result queries logged (query + center + timestamp) → analytics "Content gaps" list.

  • AC3 Rate-limited kb_suggest (30/min/IP) → 429 with Retry-After.

FR-8 Helpful votes

👍/👎 per article per anonymous visitor (visitor-hash dedup, one vote, changeable).

  • AC1 Vote recorded once; toggling flips it; counts update.

  • AC2 Votes surface per article in dashboard Analytics; no PII stored.

FR-9 Analytics

Per help center + per article: visitors (distinct visitor-hash per day) and pageviews, referrer hosts, on-site searches (incl. zero-results), top/bottom articles by views and by helpful ratio, subscriber count. Range selector 7/30/90 days + all-time. recharts trends.

  • AC1 A visit fires exactly one track beacon per page view; visitor id is a client-generated uuid in localStorage, stored only as SHA-256 hash + salt server-side.

  • AC2 Numbers reconcile: pageviews = beacon rows; visitors = distinct hashes/day.

  • AC3 Manager+ only; Operations/Agent/Viewer get 403.

FR-10 Subscriptions

Listing page (if enabled) shows an email box (honeypot field hidden). Subscribing stores (email, center) once.

  • AC1 Duplicate email → friendly "already subscribed" (no error spam, no duplicate row).

  • AC2 Dashboard list + CSV export + single delete.

  • AC3 Rate-limited 5/hr/IP; honeypot fill → silent success (no store).

  • AC4 Privacy disclosure added to privacy.html.

FR-11 Credits — publish charge

  • AC1 First publish of an article charges exactly 1 credit (kb_publish); ledger idempotency key kb:{articleId}:publish makes retries safe.

  • AC2 Edits, slug/meta/cover changes, unpublish, republish, delete/restore: 0 credits, always.

  • AC3 Wallet at floor (−10) → publish returns 402 CREDITS_EXHAUSTED with the same upgrade copy as ticket creation; draft saves still work.

  • AC4 Editor shows the cost before the click; success toast shows remaining balance.

FR-12 Media (images & videos)

  • AC1 Images: png/jpeg/webp/gif ≤ per-file cap; inline on public pages via cached media route; SVG rejected.

  • AC2 Uploaded videos: mp4/webm; served with HTTP Range (seeking works); bytes meter into storage.

  • AC3 Draft-article assets require a signed URL (no public enumeration); published-article assets publicly cacheable and immutable.

  • AC4 Deleting an article leaves assets orphan-safe (row retained; media route 404s once article is deleted).

Signed expiring link (7 days) rendering the draft article read-only for anyone with the link; rotating the nonce invalidates previous links; publishing does not auto-expire the link (manual rotate).

  • AC1 Link opens a preview page with draft banner + noindex.

  • AC2 Expired/tampered token → 410/404.

  • AC3 Rotate → old link dies immediately.

FR-14 Revisions

Snapshot on every save (manual + autosave counts once per save event); cap 50 per article (FIFO); view any revision; restore creates a new revision (never destroys history).

  • AC1 Cap enforced; restore round-trips content exactly.

FR-15 Directory /p/helpcenters

Public centers only (is_public + ≥1 published public article), logo/name/intro-count card, newest center first, monochrome, Powered by Kora.

  • AC1 Private/empty centers never appear. AC2 ItemList JSON-LD emitted.

FR-16 Widget help mode

window.kora = { mode: 'help', workspace: '<slug>' } (or formKey XOR workspace enforced) → launcher opens iframe /p/helpcenter/<slug>?embed=1 in existing popup/sidebox/inline modes.

  • AC1 Works with zero config beyond workspace slug; theme/color/font params still pass through as query params.

FR-17 Public API for integrations

GET /public/v1/articles (list published: filters q/status? — published only, pagination) and GET /public/v1/articles/:slug — secret-key auth, standard rate limits + 1cr/400 metering, honors center is_public.

  • AC1 Unauth 401; private center → empty list/404; drafts/unlisted excluded.

FR-18 Webhooks

article.published, article.updated (after publish-time edits) via the existing signed pipeline; subscribable like ticket events; updated fires at most once per save.

  • AC1 Both events listed in webhook UI dropdown + validated on create; payloads carry article identity, slug, title, URL.

FR-19 Kill switch

KB_KILL_SWITCH=1 → member KB routes return 503 "Help Center temporarily disabled", public routes return 404-ish maintenance response; data untouched. Rollback lever only.

FR-20 Dashboard navigation

Sidebar "Help Center" (Workspace group, all roles) + ⌘K palette entry; write controls hidden for non-authors; NOT a Settings tab; NOT in the embedded app.

4. Page specifications (layout-level)

4.1 /dashboard/help-center (list)

Top: title + "New article" (authors) + center URL chip (copy, shows primary slug; "Claim a custom link" CTA when unclaimed). Tabs: Articles | Analytics | Subscribers | Settings. Articles tab: search, status filter (All/Drafts/Published/Trash), table (Title+cover thumb, Status pill, Visibility pill, SEO on/off dot, Views, 👍%, Updated), row menu (Edit, Share draft, Duplicate, Delete/Restore). Empty state: 2 template cards ("How-to guide", "Troubleshooting") + blank article. Analytics tab: range selector; stat tiles (Visitors, Pageviews, Subscribers, Avg helpful %); line chart (visitors/pageviews by day); lists: top articles (views), referrers, searches with zero-result badge, votes summary. Subscribers tab: count, table (email, date), CSV export, delete. Settings tab: identity card (slug manager: current primary big, alias list with 301 note, claim input with live validation, "N of 5 claims used", set-primary control); visibility card (is_public switch + plain-English explanation); branding card (color pickers with contrast check, font pickers w/ live preview line); subscription card (toggle + preview of the box); meta card (title/intro/meta fields).

4.2 /dashboard/help-center/:id (editor)

Full-page (no dashboard padding chrome beyond top bar). Left 70%: title (h1-scale input), TipTap canvas (max-w-prose), slash/bubble menus, bottom helper bar (word count, saved-at, ⌘S hint). Right 30% rail (collapsible): Status card (draft/published pills, Publish button w/ credit label, Unpublish, visibility switch, SEO switch + explanation), Share draft (link + copy + rotate), Details (slug w/ URL preview, summary, cover upload/remove, meta fields with char counters). Revisions drawer overlays from right (list: time, author, restore button, view diff-lite = render preview). Mobile: rail becomes bottom sheet.

4.3 Public listing /p/helpcenter/:ident

See FR-5. Card grid (1-col mobile, 2-col ≥sm, 3-col ≥lg when covers used; text-only cards when no cover). Search results replace grid with result list + "Clear". Subscribe box after articles (not blocking reads). Footer: center name · "Powered by Kora" (link) · theme toggle top-right.

4.4 Public article /p/helpcenter/:ident/:slug

See FR-6. Max-w-prose body; covers full-bleed within container; sticky mini-header on scroll (center name ← back). Vote row post-body. Share row (copy/X/LinkedIn). "More from " 3 cards. JSON-LD Article + BreadcrumbList.

4.5 Directory /p/helpcenters

Hero: "Help centers powered by Kora" + search-as-you-type filter. Cards: logo/monogram, name, article count, newest-article date. ItemList JSON-LD. CTA: "Create your help center" → /auth.

4.6 Draft preview /p/helpcenter/preview/:token

Read-only article render + amber "Draft preview — link expires " banner + noindex.

5. API catalog (summary — full spec in TechDoc §4–5)

Member (Bearer): GET/POST /workspace/:id/kb/articles · GET/PUT/DELETE …/articles/:aid · POST …/articles/:aid/{publish,unpublish,restore,share,share-rotate} · GET …/articles/:aid/revisions · POST …/revisions/:rid/restore · GET/PUT …/kb/settings · GET/POST …/kb/slugs · POST …/kb/slugs/:sid/primary · GET …/kb/analytics?days= · GET …/kb/subscribers(.csv) · DELETE …/kb/subscribers/:sid · GET …/kb/search-terms.

Public (no auth): GET /public/v1/kb/:ident · GET …/kb/:ident/articles/:slug · GET …/kb/:ident/search?q= · POST …/kb/:ident/track · POST …/kb/:ident/subscribe · POST …/kb/:ident/articles/:slug/vote · GET /public/v1/kb/preview/:token · GET /public/v1/kb/media/:key · GET /public/v1/kb/seo-pages (build-time feed) · GET /public/v1/kb/directory (for prerender).

Public API (secret key): GET /public/v1/articles · GET /public/v1/articles/:slug.

Upload: existing POST /attachments/upload gains attachment_type:'kb' (→ kb_assets + kb/ R2 prefix + storage gate).

Webhooks: article.published · article.updated. Cron: step 11 prune raw pageviews/searches >180d.

6. Data concepts (summary — DDL in TechDoc §3)

kb_articles (content_json + content_text + cover_key + meta + status/visibility/is_seo + share_nonce + first_publish_charged + soft delete) · kb_slugs (global slug namespace, is_primary, ≤5/ws) · kb_settings (is_public, title/intro/meta, accent, light/dark fg+bg, title/body font, subs_enabled) · kb_assets (R2 keys, sizes — metered) · kb_article_revisions (cap 50) · kb_votes (visitor_hash dedup) · kb_subscribers (email per center) · kb_pageviews (day-bucketed, visitor_hash, referrer) · kb_searches (query + result_count) · kb_articles_fts (FTS5 external content).

7. Content plan — Kora's own help center (/p/helpcenter/kora, 30 articles)

All written through our own product (dogfood), direct-answer format (H1 = the question, first paragraph = the answer). Titles/slags:

Getting started (4): How do I create a Kora workspace? (create-workspace) · How does pay-as-you-go pricing work? (pay-as-you-go-pricing) · What do I get with my 1,100 signup credits? (signup-credits) · How do I invite my team? (invite-team) Tickets & workflow (5): How do I create my first ticket? (create-first-ticket) · Claim-based vs assignment-based workflow — which should I pick? (claim-vs-assignment) · How do I merge duplicate tickets? (merge-tickets) · How do I bulk-update tickets? (bulk-update) · What happens when a ticket is resolved or closed? (resolved-vs-closed) SLA (3): How do I set SLA policies? (set-sla-policies) · How do business hours affect SLA clocks? (sla-business-hours) · What happens when an SLA is breached? (sla-breaches) Categories (2): How do ticket categories work? (ticket-categories) · How do I let AI create categories? (ai-categories) Automation (3): How do I create an automation rule? (create-automation-rule) · Which triggers and actions are available? (automation-triggers-actions) · What is dry-run and why use it? (automation-dry-run) Help center (4): How do I publish my first article? (publish-first-article) · How do I claim a custom help-center link? (claim-helpcenter-slug) · How do I brand my help center? (brand-helpcenter) · What do article analytics tell me? (article-analytics) Credits & billing (4): How do credits work? (credits-explained) · How am I billed for storage? (storage-billing) · How do I top up? (top-up) · Where do I find invoices? (invoices) AI (2): What does the AI Analyst do? (ai-analyst) · What is AI ticket structuring? (ai-structuring) Embed & widget (2): How do I embed the ticket form? (embed-ticket-form) · How do I add the help widget to my site? (help-widget) Public API (1): How do I authenticate API requests? (api-authentication) Security (1): Where is my data stored? (data-storage-eu) (30 total; write order = Getting started → Tickets → Credits → Help center self-referential → rest.)

8. E2E test-case catalog (full tables → append to Kora_E2E_TestCases.md §21 at ship)

Prefix KB-. [A]=automated (vitest), [S]=self-tested by agent on prod, [M]=manual owner.

9. Marketing & SEO lockstep (shipped with the feature)

① Landing /knowledge-base-software (AnswerCapsule + cards + "$55/agent/mo vs 1 credit/article" table + 8-FAQ JSON-LD). ② Kora help center live + footer/nav links. ③ Compare flips (expanded-data.ts:1309 one-liner ≈40 pages; data.ts 4 rows; compare/index.tsx:23 + stale SLA row :24 fix) with value "1 credit/article". ④ llms.txt Core pages (landing + kora center); llms-full.txt ### Help Center (shipped) + pricing + endpoints; Coming Soon line removed. ⑤ changelog-data.ts entry. ⑥ docs.tsx helpcenter-overview + sidebar + DOC_SEO. ⑦ pricing.tsx credit menu row → "Help center: 1 credit per published article, edits free" + includedFeatures. ⑧ know-all-about-kora.html roadmap→shipped + pricing row. ⑨ privacy.html analytics + email-collection disclosure. ⑩ /ask prompt feature 14. ⑪ cross-links from ai-ticketing-system.tsx + best-helpdesk-software.tsx. ⑫ robots.txt Disallow: /p/helpcenter/ws_; seo change-log add ×2 (feature + content); indexnow auto via sitemap. Canonicals: primary-slug URLs; Article+dateModified+Breadcrumb JSON-LD on articles; CollectionPage/ItemList on listing/directory. LLM-SEO: direct-answer article format; capability section in llms-full; AI-citation monitoring command post-ship.

10. Privacy & compliance notes

Anonymous analytics only (hashed visitor ids, referrer hosts, no IPs/cookies beyond a first-party localStorage uuid — disclosed in privacy.html). Subscription emails stored per-center, owner-exportable, deletable. Media served with nosniff; no third-party trackers on public pages; embeds lazy-loaded from their own providers.

11. Out of scope (v1)

Multi-language · scheduled publishing · approval workflows · tags/categories · custom domains · double opt-in · article comments · author profiles · real-time collaborative editing · related-by-tag. Each has a documented upgrade path and none blocks the AI features.

Was this helpful?
Share

More from Iterato