Search & AI Chatbot
The docs site has two help affordances:
- Local full-text search — a search bar in the navbar, indexes every page at build time. No external service.
- AI chatbot — a floating help button in the lower-right that streams answers from Claude, grounded in the entire docs corpus.
Search
Powered by @easyops-cn/docusaurus-search-local. The plugin runs at build time, generates a Lucene-style index, and serves it from /search-index*.json. Zero runtime cost; everything is client-side.
Enable / config in docusaurus.config.ts:
themes: [
[
'@easyops-cn/docusaurus-search-local',
{
hashed: true, // hash the index filename for cache-busting
indexBlog: false, // we have no blog
docsRouteBasePath: '/', // because docs serve at site root
},
],
],
Rebuild after content changes (npm run build) — the index is regenerated each build.
AI Chatbot
A small Express server (server/index.ts) loads every docs/**/*.md file at startup, concatenates them into a system prompt, and streams Claude responses via Server-Sent Events. The frontend is a React widget in src/components/ChatWidget/ that injects via src/theme/Root.tsx so it appears on every page.
Architecture
┌──────────────────────────────┐ ┌────────────────────────────┐
│ Browser │ fetch │ nginx (port 443) │
│ ChatWidget streams via SSE │ ──────▶ │ proxy_pass /api/ → :3940 │
└──────────────────────────────┘ └─────────────┬──────────────┘
│
▼
┌────────────────────────────┐
│ ygc-docs-chat (systemd) │
│ Express + tsx, port 3940 │
│ loads docs/ at startup │
└─────────────┬──────────────┘
│
▼
┌────────────────────────────┐
│ api.anthropic.com │
│ claude-opus-4-7 │
│ + prompt caching │
└────────────────────────────┘
Model and caching strategy
| Setting | Value | Why |
|---|---|---|
| Model | claude-opus-4-7 | Latest Opus — most accurate Q&A. Switch to claude-sonnet-4-6 if cost matters more than peak quality. |
effort | medium | Default high is overkill for docs Q&A; medium halves token spend with imperceptible quality loss. |
cache_control: ephemeral on the system prompt | 5-minute TTL | The full docs corpus (the system prompt) is the stable prefix shared across every request. Cache hits cost ~10% of the uncached price. |
max_tokens | 4096 | Plenty for a docs answer; harder cap than the model would naturally hit. |
The cache hit/miss stats are returned in the final SSE event (cache_read_input_tokens / cache_creation_input_tokens) — useful for verifying the cache actually warms up across requests.
Local development
In one terminal — start the docs site as usual:
cd /Users/jsalinga/odoo/odoo17/custom/ygc17/ygc17-docs
npm start
# http://localhost:3000
In another terminal — start the chat server:
cd /Users/jsalinga/odoo/odoo17/custom/ygc17/ygc17-docs/server
npm install # one-time
echo "ANTHROPIC_API_KEY=sk-ant-..." > .env # one-time
npm run dev # tsx watch mode, restarts on file changes
# http://localhost:3940
The widget detects localhost and points to http://localhost:3940/api/chat directly. In production it uses the same-origin path /api/chat and nginx reverse-proxies it.
Deploying the chat server
End-to-end host setup is in Deploy the Docs Site. The short version, once the host has Node 20, the /opt/ygc-docs/{server,docs} layout, and /etc/ygc-docs-chat.env:
# From your laptop
rsync -az --exclude=node_modules --exclude=.env -e "ssh -p 827" \
server/ ubuntu@docs.yatcogroup.com:/opt/ygc-docs/server/
ssh -p 827 ubuntu@docs.yatcogroup.com 'cd /opt/ygc-docs/server && npm install'
# Install + start the systemd unit
ssh -p 827 ubuntu@docs.yatcogroup.com '
sudo cp /opt/ygc-docs/server/../*.service /etc/systemd/system/ 2>/dev/null || true
sudo systemctl daemon-reload
sudo systemctl enable --now ygc-docs-chat
'
# Verify
ssh -p 827 ubuntu@docs.yatcogroup.com 'curl -s http://localhost:3940/api/health'
# {"status":"ok","docsSize":...}
tsx is in devDependencies on purpose (it's the runtime for the TypeScript entrypoint), so use a plain npm install rather than --omit=dev.
nginx reverse proxy
The vhost at /etc/nginx/sites-available/docs.yatcogroup.com already proxies /api/ to 127.0.0.1:3940 with proxy_buffering off (required for SSE streaming) and a 600 s read timeout. The reference config lives in the repo at scripts/docs.yatcogroup.com-nginx.conf.
Verify end-to-end:
curl https://docs.yatcogroup.com/api/health
# {"status":"ok","docsSize":...}
Updating the docs corpus
The chat server loads docs/**/*.md at startup — content edits don't take effect until the service restarts. scripts/deploy.sh already does this:
ssh -p 827 ubuntu@docs.yatcogroup.com 'sudo systemctl restart ygc-docs-chat'
Cost guardrails
A typical Q&A turn (system prompt cached, ~1K user tokens, ~500 output tokens):
| Token type | Tokens | Cost (Opus 4.7) |
|---|---|---|
| Cache read (docs corpus) | ~30,000 | ~$0.015 |
| Input (user question) | ~1,000 | ~$0.005 |
| Output | ~500 | ~$0.0125 |
| Per turn | ~$0.03 |
First question of a 5-minute window pays the cache-write premium (~1.25× the read cost). Switch to claude-sonnet-4-6 to roughly halve all of these numbers if usage scales up.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Widget says "couldn't connect to the help server" | systemd unit not running | sudo systemctl status ygc-docs-chat, check sudo journalctl -u ygc-docs-chat -e |
502 Bad Gateway from nginx | Reverse proxy enabled but chat server not listening on 3940 | Confirm port with sudo ss -tlnp | grep 3940 |
| Chat says "API error 401" | ANTHROPIC_API_KEY missing/invalid | Update /etc/ygc-docs-chat.env (root, mode 600), sudo systemctl restart ygc-docs-chat |
| Tokens don't show cache hits after a few requests | System prompt changed between requests (e.g. a new doc was deployed but service wasn't restarted, leaving stale state) | Restart the service to reload docs |
| Search bar missing from navbar | Plugin not in themes array | Verify docusaurus.config.ts, rebuild |
| Search returns no results | Index out of date | Rebuild with npm run build and redeploy |