Writing and publishing articles

Every field on a knowledge-base article, the length limits that reject a save, how question text and answer HTML are cleaned differently, what unpublishing actually hides, and the ordering sentinel that used to send your top article to the bottom.

6 min readUpdated 3 August 2026articles, categories, tags, publishing

An article is one row in faqs: a question, an answer, a category, a page path, and some optional trimmings. Create them at Admin → Extensions → Business Tools → Knowledge Base → Manage, then Add FAQ.

The fields

Field Required Rules
Question Yes 10–500 characters after trimming. Stored as plain text
Answer Yes 20–10,000 characters. Stored as HTML
Category Yes 2–50 characters, letters, digits, spaces, - and _ only
Page path Yes Must start with /, max 200 characters
Tags No Up to 10, each 2–30 characters, same character set as category
Image No A site-relative path or an http(s) URL, max 191 characters
Related articles No Hand-picked, or suggested by the AI helper
Status No Defaults to published
Order No Position on its page. Auto-assigned if you leave it

Every limit above is enforced on the server, not just in the form. A save that violates one comes back as a 400 listing every failure at once, so you fix them in a single pass rather than one per attempt.

The lower bounds are the ones people trip over. A ten-character minimum on the question rules out Fees?; a twenty-character minimum on the answer rules out Yes. and No, sorry. Both are deliberate — an article that short is a search result that wastes a reader's click.

Question text and answer HTML are cleaned differently

This distinction matters if you paste content in from somewhere else.

The question is a plain-text field. Anything that looks like a tag is removed outright, along with control characters, and the result is trimmed. <b>Fees</b> is stored as Fees. There is no way to put markup in a question, and there is no reason to want one.

The answer is HTML and keeps its formatting. Headings, lists, links and emphasis all survive. What is stripped is the set of things that can execute or impersonate: <script> blocks and their contents, every on* event attribute, javascript: and data: URLs in href and src, and the iframe/embed/object/form/input/button tags. The answer is sanitised again on the way to the browser.

The image is treated as a URL, not as text. It must be site-relative or http(s); javascript:, data:, vbscript: and file: are rejected, as is anything over 191 characters — the column length. A rejected image is dropped silently rather than saved broken, so if an image URL vanishes on save, it failed one of those checks.

Plain-text fields used to be HTML-entity-encoded before storage, so What's the fee for a BTC/USDT withdrawal? was stored — and shown to every visitor — as What&#x27;s the fee for a BTC&#x2F;USDT withdrawal?. It compounded on every save, and it broke image paths the same way, which is why newly created articles rendered a broken image while editing one accidentally repaired it.

Storage is now literal and escaping happens at output. Existing rows that were mangled were mangled on disk; they need re-saving with the correct text.

Categories are just text

There is no category table, no category editor and no category record. A category is whatever string you typed into the field, and the category lists on both the admin and reader sides are a SELECT DISTINCT over the column.

The consequences are worth stating plainly:

  • A category is created by using it and destroyed by not using it. Move the last article out of "Deposits" and the category disappears from every list.
  • Spelling is the identity. Trading and trading are two categories. So are Deposits and Deposit. Nothing warns you, and the reader sees both tabs.
  • Renaming means editing every article in it. The bulk update on the admin list will do it in one action for a selection.

The reader-facing category cards pick an icon from a fixed lookup keyed on the lowercased category name. account, security, trading, wallet, deposit, withdrawal, general, kyc, payment, support, verification, fees, api and mobile each get a matching icon; everything else gets a question mark. That is the only reason to prefer one of those names — nothing else in the product treats them specially.

Tags work the same way, with one difference: the admin tag filter reads a distinct list built from every row in the table, so it lists tags used anywhere, not just tags on the articles currently on screen. Filtering by several tags is an AND — an article must carry all of them to match.

Publishing, and what unpublishing hides

Status is a boolean. Published articles are visible; unpublished ones are not.

Unpublishing holds on every reader-facing door:

  • The public list omits them.
  • The public search cannot return them.
  • The category list ignores them, so a category whose only article is unpublished disappears.
  • The article endpoint answers 404 for a direct link — not 403. Whether an id exists is itself not something an anonymous caller needs to learn.
  • The related-article panel filters them out, so an article you unpublish stops being linked to from the ones that referenced it.
  • The AI "answer this question from our library" helper only reads published articles.

That last point is easy to miss. Unpublishing is a real retraction, not a list-level hide, so it is the correct action for something that is wrong or no longer meant to be public.

The detail endpoint served unpublished articles in full to anyone holding the link. If you unpublished something sensitive on an older build and assumed it was gone, assume instead that it was readable for as long as that build was running.

Deleting is a soft delete — the row keeps its deletedAt and stops being served. Deleting an article does not remove the feedback rows attached to it, so the feedback list can show a comment about an article that no longer exists.

Order, and the sentinel that used to eat your top article

order positions an article within its page path, ascending. The article list — admin and reader alike — sorts by it.

Leave the field alone and the server assigns MAX(order) + 1 for that page, under a row lock so two people creating articles at the same moment cannot land on the same position.

The auto-assign used to be triggered by order === 0, which is also the value meaning first. An admin who deliberately typed 0 to put an article at the top of a page had it silently rewritten to the end of the queue.

The trigger is now "the field was not supplied at all". If you type 0, you get position 0. If you are on an older build, do not type 0 — reorder by dragging instead.

Dragging in the by-page view rewrites the whole page's order sequence inside one transaction, so a failed drag leaves the previous order intact rather than a half- renumbered page. See Pages and ordering.

relatedFaqIds is a hand-picked list of other articles, shown at the foot of an article. At most ten are returned, and unpublished ones are filtered out at read time — so the list can be shorter than what you selected, and you do not need to prune it when you retire something.

The AI helper will suggest them for you. It is given a shortlist of at most 200 published articles, most-viewed first, and whatever ids it returns are intersected back against that shortlist before anything is stored. That intersection is load-bearing: without it a hallucinated identifier would be written into the column and render as a dead link forever.

Bulk actions

Select rows in the admin list for:

  • Activate / deactivate — the status toggle across the selection.
  • Delete — soft-deletes the selection. Identifiers are validated as UUIDs before anything is executed.
  • Update — applies the same field values to every selected row. Only question, answer, image, category, tags, status, order, pagePath and relatedFaqIds are accepted; anything else in the request is ignored rather than written.

Bulk update is the tool for renaming a category or moving a group of articles to another page path. Note it applies the same value to every row — bulk-setting order gives every selected article the same position, which is rarely what you want.

What to write

Two habits pay off, because both feed the reporting.

Write the question the way a reader would type it, not the way your team refers to it internally. Search matches substrings of the question and answer text, so "How long does a withdrawal take?" is findable and "Withdrawal SLA" is not.

Then check the zero-result search list on the dashboard once a week and write for it. That list is generated from real reader queries and is the only place in this product that tells you what is missing rather than what exists.