Skip to content

Latest commit

 

History

History
305 lines (227 loc) · 28.4 KB

File metadata and controls

305 lines (227 loc) · 28.4 KB

Tale docs — contributor guide

Rules for writing and maintaining the Mintlify documentation under docs/. These rules are binding on every change that touches a page in the tree, supersede the shorter note in the root AGENTS.md, and are loaded automatically by agents working inside docs/.

The one rule

Documentation is part of every shipping change, not a follow-up. If a pull request alters what users see, configure, or interact with — a feature, a setting, an environment variable, an API response, a CLI flag, a removal — the same PR updates the docs in every published locale (en, de, fr). Code without docs is incomplete work and does not merge.

Everything below is mechanics for making that rule easy to follow.

Where things live

Path Role
docs/**/*.md English pages. The source tree.
docs/de/**/*.md, docs/fr/**/*.md Translated mirrors. Same tree shape as English.
docs/docs.json Mintlify navigation. Edited alongside every page addition/rename/deletion.
docs/scripts/ Bun + TypeScript tooling (frontmatter and terminology linters, broken-link checker bindings).
docs/images/ Assets. Referenced from all three locales.
docs/.locale-overrides/ Regional variant overrides for the platform UI. Not used by the Mintlify site — Mintlify's config does not accept de-AT, de-CH, fr-CH.

Mintlify Cloud builds straight from the committed repo state. None of our scripts run on their side — if it is not in git at merge time, it does not exist on the site.

Taxonomy

Docs are organized on two axes. The first axis is the top-level Mintlify tab; the second axis applies only inside the Self-hosted tab, where readers split by platform role.

Top-level tabs

Directory Tab Audience
cloud/ Cloud Managed-SaaS readers. Onboarding, billing, data residency (Switzerland/EU), trust and compliance, Cloud-specific admin.
self-hosted/ Self-hosted Operators running Tale on their own infrastructure, and end users of those instances, split by role.
platform/ Platform Product feature reference. Identical for Cloud and Self-hosted. The single source of truth for every feature — Cloud and role pages link in.
develop/ Develop API consumers, webhook integrators, SDK users, source contributors.
legal/ (footer) Privacy policy, terms of service, DPA. noindex: true in frontmatter.

Self-hosted sub-structure

Operators and end users share the tab. They live in different subdirectories so each role can be navigated in isolation.

Subdirectory Audience
install/ First-time installation — Linux, Docker Compose, Kubernetes, TLS.
cli/ The tale CLI — commands, install, troubleshooting.
configuration/ Environment variables, retention, providers, storage, networking. Authoritative reference pages.
operate/ Running a live instance — deployments, observability, backups, upgrades, advisories, release notes.
admin/ Owner and Admin workflows — members, roles, teams, auth, branding, governance, usage analytics.
developer/ Developer-role tasks — agents, automations, integrations, API keys, webhooks.
editor/ Editor-role tasks — knowledge base, conversations, approvals, products/customers/vendors.
member/ Member-role tasks — chat, read-only knowledge and conversations, preferences.

Placement rules

  • Feature reference goes under platform/. One canonical page per feature. Cloud and role pages link in; they do not re-document.
  • Deployment-only content goes under its flavour tab. Install docs only apply to Self-hosted; billing only applies to Cloud.
  • Role pages own the task, platform pages own the concept. An Editor's how-to on uploading a document lives at self-hosted/editor/knowledge-base.md; the full walkthrough lives at platform/workspace/knowledge-base.md. Never copy the walkthrough into a role page — link to it.
  • platform/ is the UI. System access lives under self-hosted/. Anything a user — including admins — does inside the running app (click a button, fill in a form, toggle a setting in Settings > …) goes under platform/. Anything that requires filesystem access, config files (TALE_CONFIG_DIR/**), environment variables, CLI commands, SOPS (the encrypted-secrets tool used for *.secrets.json files), Docker, or server-side deployment goes under self-hosted/configuration/ or self-hosted/operate/. When the same feature has both a UI path and a config-file path, platform/ describes only the UI path and links to the self-hosted reference for the file form. Never paste a JSON config snippet, a cp examples/... $TALE_CONFIG_DIR/... command, or an env-var table into a platform/ page — those contradict the Cloud reader's reality and belong one tab over.
  • Owner has no directory. Owner is Admin plus a small set of org-lifecycle actions, which live in one page: self-hosted/admin/organization-lifecycle.md.
  • Disabled has no docs. A Disabled account cannot access the product.
  • Cloud has no role split. Cloud readers consume platform/ directly; role permissions are covered by the shared canonical matrix at self-hosted/admin/members-and-roles.md (linked from Cloud admin pages).

Never mix audiences in one page. If a concept genuinely spans audiences, write two short pages that cross-link, not one hybrid page.

Worked example — UI vs system content

A provider has two surfaces. Both are legitimate, but they belong in different tabs:

Aspect Belongs in
What Settings > Providers shows, how to add/edit/delete a provider there platform/admin/providers.md
What model tags mean (chat, vision, transcription) — a product concept platform/admin/providers.md
The JSON file layout, defaults.* keys, cost.centsPerAudioMinute self-hosted/configuration/providers.md (or wherever the file form lives)
cp examples/providers/openai.json $TALE_CONFIG_DIR/providers/ self-hosted/configuration/providers.md
TALE_CONFIG_DIR value per deployment flavour self-hosted/configuration/environment-reference.md
SOPS encryption of *.secrets.json self-hosted/configuration/providers.md

A Cloud reader lands on platform/admin/providers.md, sees a cp command, and is confused — they have no shell on the instance. Keep the platform page pure: describe the UI, link out for the file form.

Writing style

Every page is judged against the same bar: a reader who lands cold from a search result should come away with the concept and the next action they can take. Thin pages that only list bullets fail that bar.

Mechanics

  • Frontmatter is required. Every page has title and description. Legal pages also carry noindex: true.
  • Filenames are dash-case. api-reference.md, never api_reference.md or APIReference.md.
  • Headings are sentence case. ## Agent concepts, not ## Agent Concepts.
  • One topic per file. When a page drifts into a second subject, split it.
  • Code blocks always carry a language identifier. ```bash, ```typescript, ```json — never a bare ```.
  • Tables stay aligned. Pipes line up, padding matches. Run bun run --filter @tale/docs format (oxfmt) before committing.
  • Imperative voice for instructions. "Run tale deploy" — never "You can run tale deploy".
  • Link to sources of truth, never copies. If an env var is documented in configuration/environment-reference.md, link to it; do not re-describe the variable inline.
  • No status chatter. Updated:, New in v1.6:, TODO: have no place in prose. Release notes and git history cover that.
  • Use Mermaid for architecture and flow diagrams. Label nodes in full sentences; size diagrams to fit on one screen.

Depth and voice

Short is good. Fragmentary is not. A page that opens with one sentence and a bullet list forces the reader to assemble the mental model themselves, and leaves non-obvious "why" decisions invisible.

  • Every page opens with a 2–4 sentence concept paragraph that explains what the feature is, who it is for, and why it exists. Single-sentence intros are a bug.
  • Explain why, not only what. Run tale deploy to apply the new config tells the reader what to type; Run tale deploy to trigger a blue-green rollout — the old container keeps serving traffic until the new one passes its health check tells them why the command is safe in production.
  • Paragraphs beat bullet lists for prose. Reserve bullets for parallel items (commands, env vars, options). A bullet list of three items explaining a concept is almost always better as a paragraph.
  • Short lists are prose. Fewer than five items? Write a sentence. Tables and bullets are for five or more parallel items.
  • Define every domain term on first use per page (The composer is the chat input at the bottom of the screen). After the first definition, use the term freely.

Depth example

The rule in practice:

Add a website

Paste the URL. Click Crawl. Wait for the pages to index.

Rewritten to meet the bar:

Add a website

Adding a website pulls every page under a URL into the knowledge base so agents can answer questions using that content. Crawling runs in the background and can take minutes or hours depending on site size; the tab does not need to stay open.

Paste the URL in the composer, open the website panel, and start a crawl. Crawl schedules and refresh cadence are covered separately in Website crawling.

Internationalization

Locales we publish

Three locales, each with full coverage: en, de, fr. English lives at the docs/ root; German and French live under docs/de/ and docs/fr/. Mintlify does not fall back across languages — a missing translated file turns into a 404 on the navigation entry.

Why three locales when the platform UI supports six? Mintlify's config schema does not accept regional codes (de-AT, de-CH, fr-CH). Those locales exist in the platform UI translation files (services/platform/messages/*.json); docs readers on those locales see the closest base. When the Mintlify enum expands, we revisit.

Lifecycle rules

When you add a page:

  1. Create the English file at docs/<path>.md.
  2. Create translated mirrors at docs/de/<path>.md and docs/fr/<path>.md.
  3. Add the page to every navigation.languages block in docs/docs.json, using locale-prefixed paths.
  4. Run bun run --filter @tale/docs format to normalize Markdown (tables, list spacing, etc.).
  5. Commit the locale files and docs.json together.

When you rename or move a page:

  1. Rename the file in every locale tree.
  2. Update the pages entry in every navigation.languages block.
  3. Grep the repo for the old path (at minimum README.md and the sibling locales) and update references.

When you delete a page:

  1. Delete from every locale tree.
  2. Remove from every navigation.languages block.

Editing rules

  • Locale-prefixed internal links in non-English files. A link in docs/de/build/agents/create.md points to /de/build/agents/concepts, not /build/agents/concepts.
  • Translate every frontmatter value. Both title and description. A German page with an English title is a bug.
  • Code and diagram syntax stays put. Inside fenced code, <CodeGroup>, and Mermaid DSL, translate only human-readable node labels. Never the arrows, participant keywords, or block structure.
  • Brand names never translate. Tale, Convex, Mintlify, OpenRouter, Claude, GitHub, Slack, Gmail, Outlook, Shopify — all stay as-is in every locale.
  • Keep anchors stable. Mintlify slugs headings; when you change a heading in one locale, update every locale that links to the anchor, since the generated slug differs per locale.

Translation style

Style rules in short:

  • Informal form everywhere — du in German, tu in French. Never Sie or vous.
  • Sentence case in headings in every locale.
  • ICU placeholders preserved exactly{count, plural, ...}, {field}. Rare in docs; flag any you see broken.

Translate meaning, not words

English-to-German and English-to-French are not word-substitution problems. Sentence structure, idiom, and noun choice all differ across languages. A mechanical, word-for-word render produces sentences native readers reject — even when every individual word is correct.

Concrete rules, every one of which has failed in this repo before:

  • Never calque English metaphors. Published certification story was once rendered into German as "eine veröffentlichte Zertifizierungsgeschichte" — literally "a published history of certifications," a phrase no German speaker would write. The natural rendering names the certifications: "ISO 27001, SOC 2 Type II und DSGVO-Konformität". When English reaches for a figurative noun (story, journey, posture, surface), translate its meaning, not the noun.
  • Don't borrow English when the target language owns a native word. Surface opérationnelle is an Anglicism; French uses l'exploitation. Operative Seite is awkward German; prefer der Betrieb. Verify the loanword actually exists in the target language with your intended meaning — many do not.
  • Restructure sentences to fit the target language. German compound nouns and verb-final subordinate clauses; French preference for relative clauses over stacked noun phrases. If English uses three short clauses and the natural German equivalent is one longer sentence, write the longer sentence.
  • Prefer concrete nouns to abstract ones. English tech prose leans on abstractions (posture, story, flow); most readers in the target language prefer the concrete thing. Trust postureunsere Zertifizierungen / nos certifications publiques.
  • Read the paragraph aloud. If it sounds like a translation, rewrite it. A good translation reads as if originally authored in the target language.
  • When in doubt, drop the figure of speech. Stating the underlying fact plainly beats a literal rendering that reads as machine-generated.

UI terms must match the locale's shipped label

Every user-facing term a doc page names — a button, a menu item, a panel title, a feature, a knowledge-base entity — must match the string the UI actually displays in that locale, verbatim. The source of truth is services/platform/messages/<locale>.json. If the German UI shows Kunden and your page writes Customers, the reader cannot find what you point at. Mixed forms (half English, half translated) in the same sentence are the most common bug — do not write them.

Rules:

  1. services/platform/messages/<locale>.json is the single source of truth. Terminology files document it; docs quote it. Before writing a UI term in a translated page, grep the locale JSON for its key (navigation.*, settings.*.title, <entity>.title, chat.*). If the UI string and the terminology file disagree, the UI wins — update the terminology file to match, then the doc. Never pick the English term because it "reads better".
  2. Don't carry English over as a loanword unless the UI itself does. Canvas stays Canvas in German (UI shows Canvas) but becomes Canevas in French (UI shows Canevas). The .agents/TERMINOLOGY_<LOCALE>.md tables are the authoritative mapping — update them if the UI changes.
  3. Code identifiers stay English. CLI flags (tale deploy --detach), env vars (TALE_CONFIG_DIR), file paths (docker-compose.yml), i18n keys (chat.canvas.title), API paths (POST /api/v1/documents) are international and never translate. Inside a sentence in a translated page, quote code as code — do not paraphrase the path.
  4. Role names stay English in every locale. Owner, Admin, Developer, Editor, Member, Disabled — because the UI ships them that way. Generic members of a team becomes Mitglieder / membres; the capital-M role stays Member.
  5. Parenthetical lists translate too. When an English page writes (Products, Customers, Vendors) as examples, the German mirror writes (Produkte, Kunden, Lieferanten) and the French mirror writes (Produits, Clients, Fournisseurs). Don't leave the English list behind — it contradicts the UI the reader just opened.
  6. Navigation paths translate segment by segment. Settings > Members becomes Einstellungen > Mitglieder / Paramètres > Membres. Writing Einstellungen > Members is a bug: the reader sees Einstellungen in the sidebar but no Members entry.

Canonical UI label reference

Quote these values verbatim in every translated page. If a term you need is missing from the table, grep the locale JSON — then add the entry to the matching .agents/TERMINOLOGY_<LOCALE>.md file so the next edit is cheap.

English German (de) French (fr) Source key
Customers Kunden Clients customers.title
Products Produkte Produits products.title
Vendors Lieferanten Fournisseurs vendors.title
Documents Dokumente Documents documents.title
Websites Websites Sites web websites.title
Conversations Konversationen Conversations navigation.conversations
Knowledge Wissen Base de connaissances navigation.knowledge
Knowledge base Wissensdatenbank Base de connaissances terminology
Automations Automatisierungen Automatisations navigation.automations
Integrations Integrationen Intégrations navigation.integrations
Teams Teams Équipes navigation.teams
Agents Agents Agents navigation.agents
MCP servers MCP-Server Serveurs MCP navigation.mcpServers
Providers KI-Anbieter Fournisseurs IA navigation.providers
API keys API-Schlüssel Clés API navigation.apiKeys
Branding Branding Image de marque navigation.branding
Governance Richtlinien Gouvernance navigation.governance
API docs API-Dokumentation Documentation API navigation.apiDocs
Account Konto Compte navigation.account
Logs Protokolle Journaux navigation.logs
Organization Organisation Organisation navigation.organization
Settings Einstellungen Paramètres navigation.settings
Members (entity) Mitglieder Membres terminology — Member as a role stays English
Approvals Genehmigungen Approbations terminology
Chat with AI Chat mit KI Discuter avec l'IA navigation.chatWithAI
Canvas Canvas Canevas chat.canvas.title
Composer Composer Composeur composer.*
Prompt library Prompt-Bibliothek Bibliothèque de prompts chat.promptLibrary
Arena Mode Arena-Modus Mode Arène chat.arena.title
Research plan Recherche-Plan Plan de recherche todoList.title
Thread (prose) Konversation Conversation terminology — keep Thread only in code/API identifiers

Worked examples:

Scenario Wrong Right
German page listing structured-data entities "Importiere Products, Customers, Vendors" "Importiere Produkte, Kunden, Lieferanten"
French page listing structured-data entities "Importe Products, Customers, Vendors" "Importe Produits, Clients, Fournisseurs"
German page naming a settings sub-page "Gehe zu Einstellungen > Members" "Gehe zu Einstellungen > Mitglieder"
German page naming the conversations inbox "Conversations ist das Kunden-Inbox" "Konversationen ist der Kunden-Posteingang"
German page naming the canvas feature "Öffne das Canvas Panel" "Öffne das Canvas" (matches chat.canvas.title)
French page naming the canvas feature "Ouvre le panneau Canvas" "Ouvre le Canevas" (matches chat.canvas.title)
German page naming the prompt library "Öffne die Prompt Library" "Öffne die Prompt-Bibliothek" (matches chat.promptLibrary)
French page naming the prompt library "Ouvre la Prompt Library" "Ouvre la Bibliothèque de prompts" (matches chat.promptLibrary)
German page naming the research plan pane "Öffne die Todo-Liste" "Öffne den Recherche-Plan" (matches todoList.title)
French page naming the research plan pane "Ouvre la Todo list" "Ouvre le Plan de recherche" (matches todoList.title)
German page naming the conversation starters feature "## Conversation Starter" "## Gesprächseinstiege" (matches the agent UI)

When unsure, grep the locale JSON:

grep -F '"Kunden"' services/platform/messages/de.json
grep -E '"(title|label)":' services/platform/messages/fr.json | grep -i client

Before opening a PR that touches a translated page, grep your own diff for English UI nouns (Customers, Products, Vendors, Documents, Websites, Conversations, Members, Settings, Approvals, Knowledge Base, Conversation Starters, Prompt Library, Todo List). Any hit in docs/de/ or docs/fr/ that is not inside a fenced code block, an i18n key, a URL, or an English brand name is a bug.

Workflow

Local preview

cd docs
bun install        # first time only
bun run dev        # predev (table formatter) + mintlify dev

Click through the language switcher on every section on every locale. A 404 in any locale means a missing file or a stale docs.json entry.

Before every PR

All three must pass:

bun run --filter @tale/docs format         # oxfmt: normalize Markdown and JSON
bun run --filter @tale/docs lint            # frontmatter + terminology + Mintlify broken-link check

Navigation parity

Every pages entry across the three navigation.languages blocks must resolve to a real .md / .mdx file. A quick drift check:

cd docs && node -e "
const j = JSON.parse(require('fs').readFileSync('docs.json', 'utf8'));
const fs = require('fs');
function collect(n, o=[]) { for (const e of n) typeof e === 'string' ? o.push(e) : collect(e.pages, o); return o; }
for (const l of j.navigation.languages) for (const p of collect(l.groups)) {
  if (!fs.existsSync(p + '.md') && !fs.existsSync(p + '.mdx')) console.log('MISSING', l.language, p);
}
"

Common pitfalls

  • Forgetting a navigation.languages block. A file on disk but not in docs.json is invisible in that locale.
  • Translated anchors that don't match their target. /de/bar#some-heading only works if docs/de/bar.md has a heading whose German slug is some-heading.
  • External links cast as internal. ](/external-site) is treated as in-site and 404s. External links are fully qualified (https://…).
  • Committing without running format. Run it first so reviewers don't wade through alignment or whitespace noise.
  • Duplicating env var or API reference content. The reference pages are authoritative — link to them.