| name | mkdocs-material-deployment |
| description | Deploy MkDocs Material sites to Docker, Cloudflare Pages, and GitHub Pages. Covers CSS loading bugs, directory flattening, nginx config, theme overrides, search index limits, multi-environment consistency, plus the wikiโbook compilation pipeline (book_compiler, chapter splitting, wikilink preprocessing), E2E testing, and operational fixes. Use when building, deploying, or debugging MkDocs Material sites. |
MkDocs Material Deployment
Deploy MkDocs Material sites to multiple environments (Docker + Cloudflare Pages + GitHub Pages) without breaking each other.
When to Load This Skill
- Building/deploying MkDocs Material sites
- Fixing CSS/layout issues in MkDocs sites
- Deploying to Docker + Cloudflare Pages + GitHub Pages
- Debugging broken links or styles in MkDocs output
CSS Loading โ <base href="/"> Depends on Deployment Context
The rule is context-dependent, NOT a blanket ban.
When <base href="/"> IS needed (Docker/nginx clean URLs)
When nginx serves clean URLs (e.g., /ch01-ai-basics/ maps to ch01-ai-basics.html), the browser thinks it's in a subdirectory. Relative CSS paths like href=assets/stylesheets/main.xxx.css resolve to /ch01-ai-basics/assets/... โ 404.
Fix: Add <base href="/"> to overrides/main.html:
{% extends "base.html" %}
{% block site_meta %}
{{ super() }}
<base href="/">
{% endblock %}
When <base href="/"> BREAKS things (GitHub Pages subpath)
GitHub Pages serves at /<repo-name>/ (e.g., /wiki-book/). A <base href="/"> causes all relative links to resolve to root (/) instead of /wiki-book/.
Fix: Do NOT use <base> tag. Instead ensure use_directory_urls: false in mkdocs.yml so links are explicit .html paths.
Decision matrix
| Environment | Clean URLs? | <base href="/">? | Why |
|---|
Docker/nginx (root /) | Yes | YES | Flat files served at subdirectory paths break CSS |
CF Pages (root /) | No (static) | Optional | Files at actual paths, relative works |
| GitHub Pages (custom domain) | No | NO | Custom domain serves at root, no subpath issue |
GitHub Pages (/repo/ subpath) | No | NO | Breaks subpath resolution |
Pitfalls:
- Block name is
site_meta, NOT head
- Dockerfile must
COPY overrides/ overrides/ before RUN mkdocs build
- If you have BOTH Docker and GitHub Pages deploys, use conditional Jinja or maintain separate overrides
GitHub Pages Subpath CSS Breakage (Critical)
When deploying to GitHub Pages WITHOUT a custom domain, the site lives at https://<user>.github.io/<repo>/. MkDocs with site_url: https://custom.domain generates root-relative CSS/JS paths (/assets/stylesheets/main.xxx.css) that resolve to https://<user>.github.io/assets/... instead of https://<user>.github.io/<repo>/assets/... โ all styles 404.
Root cause: site_url in mkdocs.yml must match the ACTUAL deployment URL. If using a custom domain, that domain must have DNS configured โ otherwise GitHub Pages falls back to the subpath silently.
Fix (two options):
Option A: Custom domain (preferred)
- Create
docs/CNAME with the custom domain (e.g., wiki.jinguo.tech)
- Set GitHub Pages custom domain:
gh api repos/<user>/<repo>/pages --method PUT --input - <<'EOF'
{"cname": "wiki.jinguo.tech", "source": {"branch": "main", "path": "/"}}
EOF
- Add DNS record:
CNAME wiki โ <user>.github.io (gray cloud, DNS-only)
- Keep
site_url: https://wiki.jinguo.tech in mkdocs.yml
Option B: Subpath-relative paths (no custom domain)
Change site_url to match the subpath:
site_url: https://<user>.github.io/<repo>/
Then rebuild โ MkDocs generates paths with <repo>/ prefix.
Pitfall: If you set a custom domain in GitHub Pages but the DNS record doesn't exist, the site falls back to subpath silently. Always verify DNS propagation before assuming custom domain works.
Duplicate Files: Flat vs Nested
MkDocs with use_directory_urls: true (default) generates BOTH:
- Flat:
ch01-001-xxx.html (at site root)
- Nested:
ch01/001-xxx.html (in subdirectory)
Which to keep depends on which format the HTML links use.
Check the actual link format in chapter index pages:
grep -o 'href=[^ >]*\.html' site/ch01-ai-basics.html | head -5
If links use subdirectory format (ch01/001-xxx.html) โ KEEP subdirs, DELETE flats:
RUN cd site && find . -maxdepth 1 -name 'ch[0-9][0-9]-[0-9][0-9][0-9]-*.html' -delete
If links use flat format (ch01-001-xxx.html) โ DELETE subdirs, KEEP flats:
RUN cd site && for d in ch01 ch02 ch03 ...; do rm -rf "$d" 2>/dev/null; done
CRITICAL: Never delete the format that links point to. Verify BEFORE writing the Dockerfile step.
Pitfall (discovered 2026-06-30): The old advice was "always delete subdirs." This broke wiki-book because chapter index pages used ch01/001-xxx.html links. After deleting subdirs, every article link 404'd.
Cloudflare Pages .html Redirect Pitfall (Critical)
When deployed via Cloudflare Pages, .html extension URLs trigger a 308 permanent redirect to the clean URL (e.g., /ch01/001-2026-15.html โ /ch01/001-2026-15). This is built-in Cloudflare Pages behavior and cannot be disabled.
The problem: In some browsers or cached states, this 308 redirect can misresolve to a broken URL, replacing / with - (e.g., /ch01/001-2026-15 becomes /ch01-001-2026-15 โ 404).
Fix: Remove .md extensions from source markdown links so MkDocs generates clean URLs directly (no .html โ no redirect).
In split-chapters.py (or equivalent link-generation script):
# BEFORE โ generates <a href="ch01/001-2026-15.html">
index_lines.append(f"- [{num}. {title}]({ch_prefix}/{num}-{slug}.md)\n")
# AFTER โ generates <a href="ch01/001-2026-15">
index_lines.append(f"- [{num}. {title}]({ch_prefix}/{num}-{slug})\n")
nginx side: Must handle clean URLs via try_files:
location / {
try_files $uri $uri.html $uri/index.html =404;
}
Detection: After clicking an article link from the chapter index page, check window.location.href โ if it shows ch01-001... (dash) instead of ch01/001... (slash), the 308 redirect misresolved.
Search Index Size Limits
search_index.json grows with content. Two CF Pages limits apply:
25MB per-file limit: A full rebuild can produce a 247MB search index, exceeding CF Pages' 25MB per-file cap:
Error: Pages only supports files up to 25 MiB in size
Fix: Remove the search index from deploy dir (it's uploaded to R2 separately):
rm -f site/search/search_index.json
wrangler pages deploy site ...
20,000 file limit (free plan): Switching use_directory_urls settings creates double files (flat + nested), hitting the cap.
- Syptom:
Pages only supports up to 20,000 files in a deployment for your current plan
- Fix: Always
rm -rf site/* && mkdocs build after changing use_directory_urls
- Never rely on
--dirty across a config change โ it leaves stale files from both modes.
Chapter Index Link Format
Links in chapter index files must match the ACTUAL filename (which may contain Chinese characters). Use relative .html format:
โ ](/ch01-001-2026-15/) # truncated English slug, file doesn't exist
โ ](ch01/001-xxx.md) # nested path, wrong
โ
](ch01-001-2026ๅนด...ๆทฑๅบฆ่งฃ่ฏป.html) # matches actual filename
Root cause: book compilation generates filenames from Chinese titles, but link-fix scripts may use English slugs from the wiki repo. Always verify chNN-NNN prefix maps to the actual file.
Batch fix script (map prefix โ actual filename):
import os, re
file_map = {}
for f in os.listdir('docs'):
m = re.match(r'(ch\d+-\d+)', f.replace('.md',''))
if m: file_map[m.group(1)] = f.replace('.md','')
# Then replace ](/chNN-NNN-slug/) โ ](actual.html)
Search Index Optimization (Critical)
search_index.json is ~68MB for 2000+ articles. This causes Chrome tab crashes (Error code 5) when browsing. MUST slim after every mkdocs build.
Always run after build:
python3 scripts/slim-search-index.py
# 68MB โ 8MB (71772 โ 21185 entries)
The slim script:
- Keeps max 5 entries per page, 300 char text
- Excludes chapter index pages and references
- Result (~8MB) is under CF Pages 25MB limit
Build pipeline:
# scripts/build.sh
docker run --rm -v "$(pwd):/build" -w /build wiki-book-builder:latest mkdocs build
python3 scripts/slim-search-index.py
CF Pages deploy: run slim BEFORE deploy, not find -size +25M -delete (which destroys the index):
python3 scripts/slim-search-index.py # reduces to ~8MB
npx wrangler pages deploy site --project-name=...
nginx Configuration
Don't use rewrite ... permanent โ it generates redirects with Docker's internal port:
# WRONG: changes port from 8002 to 8080
rewrite ^/(.*)/$ /$1 permanent;
# RIGHT: serve directly
location ~ ^(.+)/$ {
try_files $1 $1.html $1/index.html =404;
}
location / {
try_files $uri $uri.html $uri/index.html =404;
}
Multi-Environment Deploy
docs/ โ Docker build (inside container) โ localhost:8002
docs/ โ local mkdocs build โ site/
site/ โ wrangler deploy โ Cloudflare Pages
docs/ โ git push โ GitHub Actions โ GitHub Pages
Critical: Docker build and local site/ are INDEPENDENT. Editing docs + docker compose up -d --build does NOT update site/. Must rebuild site/ separately for Cloudflare deployment.
Rebuild Commands
# Docker (picks up source changes)
docker compose down && docker compose create && docker compose start
# Local site/ (for Cloudflare)
docker run --rm -v "$(pwd):/build" -w /build <builder-image> mkdocs build
# Cloudflare
npx wrangler pages deploy site --project-name=<name>
# GitHub
git add -A && git commit && git push
Wiki-Book Pipeline, Compilation & Operations (absorbed skills)
Three absorbed skills keep their full content as references under this umbrella:
references/wiki-book-pipeline.md (absorbed wiki-book-deployment) โ the end-to-end wikiโbookโsite pipeline for ~/wiki-book: book_compiler.py โ mkdocs_prepare.py โ split-chapters.py โ fix-docs-links.py โ sync-wiki-book.sh โ full mkdocs build (never --dirty) โ slim-search-index.py โ 3-env deploy. Includes: root vs deploy/docker/ Dockerfile trap, .dockerignore site/ exclusion, CF Pages 20K-file / 25MB-per-file limits (delete search_index.json AND neighbor_graph.json before wrangler pages deploy โ ordering matters), .html-not-.md link suffixes, articles.json staleness, dashboard-only deploy, article-numbering shifts on recompilation, and its dated session references (docker-build-timeout, extended-links regression, proxy clean-build, dashboard-404, tag-comparison workflow).
references/wiki-book-operations.md (absorbed wiki-book-operations) โ operational playbook: 7-phase E2E testing (HTTP status โ HTML structure โ internal links โ Playwright rendering โ link jumps โ RAG query test โ report; test against localhost:8002, Cloudflare blocks headless Playwright), prev/next navigation JS (URL pattern matching, cache-busting), entity page URL structure, .mdโ.html wikilink conversion (must be in mkdocs_prepare.py, NOT fix-docs-links.py), htmlmin exclude_docs: raw/, PATH.md {{BASE_URL}} link wrapping, dashboard 404 diagnosis (articles.json / articleUrl() / D1 orphaned records), batch h1 downgrade, article-count data sources (three different counters โ entities vs raw vs sub-pages), deploy chains for index.md and references.md.
references/knowledge-base-to-book.md (absorbed knowledge-base-to-book) โ generic wikiโbook compilation methodology: 5-level entity classification (tag scoring, not first-match), 20-chapter topic scoring, wikilink preprocessing for MkDocs/pandoc, Chinese-content MkDocs config (templates/mkdocs.yml), EPUB generation via pandoc, and pitfalls (YAML --- separators, blob: URLs, citation-in-URL corruption).
Also absorbed: scripts/fix-path-entity-links.py, scripts/fix-flat-filenames.py (from wiki-book-operations), references/preprocessing.py + templates/mkdocs.yml (from knowledge-base-to-book).
Reference Files
references/r2-search-index.md โ R2 integration for large search indexes
references/byok-config-form.md โ BYOK config form pattern (endpoint + API key + model inputs with presets)
references/video-export-via-playwright.md โ Video export using app-as-renderer pattern
references/duplicate-files-in-compiled-outputs.md โ Detecting flat/nested duplicates and slug variants
references/escaped-characters-in-articles.md โ Fixing \\n \\t \\" in ingested content
references/css-customization-pitfalls.md โ CSS specificity issues, !important requirements, key selectors for sidebar/footer, dark mode, gradient effects
references/wiki-book-content-remediation.md โ Batch h1 downgrade, HTML residue cleanup, entity reference path bugs, Playwright E2E testing methodology for 4000+ article sites
references/playwright-ai-chat-e2e.md โ Playwright E2E test script for AI Chat panel (trigger, settings, send message, TTS, clear, close)
User Preference: Compact Footer
User prefers tight, minimal-whitespace footer design. When styling .md-footer, start with compact values:
- Footer card padding:
0.25rem 0.65rem (NOT 0.75rem 1.1rem)
- Footer inner padding:
0.15rem 1.2rem
- Meta bar padding:
0.25rem 1.2rem
- Direction label font:
0.55rem
- Title font:
0.75rem
Anti-pattern: Do NOT start with generous padding (0.75rem+) โ user will immediately ask to reduce. Start tight, expand only if asked.
Full compact footer CSS: references/css-customization-pitfalls.md โ "Compact Footer Styling" section.
Pitfalls
site/ output: MkDocs compiles .md โ .html
Post-build scripts that reference files in site/ must use .html extension, NOT .md. MkDocs always compiles Markdown to HTML. For example, site/PATH.md does not exist โ the file is site/PATH.html.
# WRONG โ file not found:
cp "$BOOK/site/PATH.md" /tmp/
sed -i 's|{{BASE_URL}}||g' "$BOOK/site/PATH.md"
# RIGHT:
cp "$BOOK/site/PATH.html" /tmp/
sed -i 's|{{BASE_URL}}||g' "$BOOK/site/PATH.html"
Detection: Script exits with cp: ... No such file or directory at a step operating on a .md path in site/.
CI OOM: Large-Scale Build Steps on GitHub Actions
Symptom: mkdocs build or post-build scripts (e.g., neighbor-graph generation) work fine locally (M1/M2/M3/M4/M5 with 16GB+ RAM) but fail silently or timeout on GitHub Actions runners (2-core, 7GB RAM).
Root cause: GitHub Actions free-tier runners have limited memory. Steps that work on local Apple Silicon machines can hit OOM on the shared runner, especially:
- Large-scale content processing (60K+ documents, sparse matrix operations like 63Kร366K)
- Search index generation with 2000+ articles
- Post-build aggregation/analysis scripts
Fix options:
-
Remove the step from CI (if the step is informational, not build-critical):
# BEFORE:
- run: python3 scripts/build-neighbor-graph.py
# AFTER: removed entirely
-
Run locally: Move memory-intensive steps to a local cron or manual trigger
-
Upgrade runner: Use ubuntu-latest-m (16GB) or self-hosted runner for heavy steps
Detection:
- CI passes
mkdocs build but fails on post-build step
- Logs show Killed: 9 or process exit code 137
- The step runs fine on
--help or with minimal input
Pitfall: A removed step is gone from CI but may still be referenced in docs or local scripts. Remove from both CI YAML AND the build wrapper script (scripts/build.sh), otherwise the wrapper references a script that no longer runs and the user mistakenly thinks it's still active.
docker compose build --no-cache pulls base images from Docker Hub. If auth.docker.io is unreachable (common behind proxies or in China), the build hangs on load metadata for docker.io/library/nginx:alpine.
Fix: Pre-pull the base images separately first โ they often succeed with retries:
docker pull nginx:alpine # pull separately with longer timeout
docker pull python:3.12-slim
docker compose build --no-cache # now uses local cache for base images
Multiple Dockerfiles
If both ./Dockerfile and ./deploy/docker/Dockerfile exist, docker compose uses ./Dockerfile (root). Changes to deploy/docker/Dockerfile won't take effect unless docker-compose.yml explicitly references it. Always check which file docker compose build actually uses โ look at the #2 [internal] load build definition from Dockerfile line in build output.
Escaped Characters in Ingested Articles
Wiki entity ingestion sometimes preserves literal \\n, \\t, \\" instead of converting to actual characters. Result: entire article content appears as one unreadable line in H1 heading.
Detection:
grep -rl '\\\\n' docs/ch*.md | wc -l
Fix (batch):
content = content.replace('\\\\n', '\n')
content = content.replace('\\"', '"')
content = content.replace('\\t', '\t')
Slug-ified Headings
Some articles have H1/H2 headings that are the filename slug (e.g., # ๅๅคๅผไธไธชๆฐๅไป้ถๅคๅปไธไธช-claude-codenn็ฎๆ ...) instead of the actual title. The real title is usually the second H1.
Detection: H1 > 200 chars or contains -codenn/-ha.md patterns.
Fix: Remove slug H1/H2 + metadata block, keep the readable H1.
CSS Customization (Critical Pitfall)
MkDocs Material's built-in CSS silently overrides custom styles. Every property the theme also sets needs !important โ including pseudo-element content, position, background.
Quick check: If your CSS isn't taking effect, run getComputedStyle(el).propName in the browser console. If the value differs, add !important.
Full reference: references/css-customization-pitfalls.md โ key selectors for sidebar/footer, dark mode patterns, gradient effects, hover animations.
Embedding AI Chat in Sidebar (CORS Proxy Pattern)
When adding a browser-side LLM chat panel to MkDocs Material, direct API calls from the browser fail due to CORS. Solution: Cloudflare Worker as a transparent proxy.
Architecture:
Browser โ CF Worker (CORS proxy) โ LLM API (OpenRouter / ่ฎฏ้ฃ / DeepSeek / etc.)
Why Worker, not direct fetch:
- Most LLM APIs (except OpenAI/OpenRouter) don't send CORS headers
- Worker hides API Key from frontend
- Single proxy works for all providers (OpenAI-compatible format)
Implementation (3 files):
overrides/assets/stylesheets/ai-chat.css โ chat panel styling
overrides/assets/javascripts/ai-chat.js โ chat logic + streaming SSE
overrides/main.html โ inject CSS/JS via styles/scripts blocks
Key JS pattern โ pass config via custom headers, not URL/body:
fetch('https://ai-chat-proxy.workers.dev', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Base': cfg.apiBase, // e.g. https://maas-api.xf-yun.com/v2
'X-API-Key': cfg.apiKey, // hidden from frontend
'X-Model': cfg.model // e.g. xopqwen36v35b
},
body: JSON.stringify({ messages, stream: true })
});
Worker implementation: references/ai-chat-cors-proxy.md โ covers BOTH modes below.
Model Configuration
Two UI patterns depending on mode:
Server-side key mode โ Model cards (visitors pick from preset list, no API key input):
- โ๏ธ gear button in header toggles
.ai-chat__settings div
- Model cards are
<button class="ai-chat__model-card" data-model="..."> with .active highlight
localStorage.getItem("ai-chat-model") persists selection
- Pass
model: getSelectedModel() in request body
- Worker must respect
payload.model over env.MODEL
BYOK mode โ Full config form (users enter their own endpoint + API key + model):
- โ๏ธ gear button toggles settings panel with preset dropdown + input fields
- Preset dropdown auto-fills endpoint/model for known providers (DeepSeek, ่ฎฏ้ฃ, MiMo, OpenAI-compatible)
- Fields: Endpoint URL (text), API Key (password), Model ID (text)
- Save button writes to
localStorage.getItem("ai-chat-config") as JSON {endpoint, apiKey, model}
- On chat send: if config has endpoint+key โ direct fetch to user's API; else โ fall back to proxy
- Direct fetch:
Authorization: Bearer <key> header, model in body
- Dual-service: If widget has TTS, add TTS Endpoint/Key/Model fields below chat fields, separated by divider. Save button at VERY BOTTOM of entire form. Full pattern:
references/byok-config-form.md โ "Dual-Service Config Form" section.
Pitfall: If the Worker ignores the model field in the request body, model switching has zero effect. Always verify Worker code reads payload.model.
Full implementation: references/ai-chat-cors-proxy.md โ "BYOK Config Form" section.
Full implementation: references/ai-chat-cors-proxy.md โ "Model Configuration UI" section.
Two Proxy Modes (Critical)
| Mode | API Key Location | User Config Needed? | Use Case |
|---|
| Frontend-provided | Browser localStorage | Yes (each user enters their own key) | Internal/dev tools, multi-user BYOK |
| Server-side | Worker env or hardcoded | No (zero-config) | Public sites, shared knowledge bases |
Server-side mode (preferred for public sites):
- Worker holds API key + base + model โ frontend sends NO key headers
- Remove settings panel from JS entirely
- TTS proxy already follows this pattern (see
deploy/cloudflare/tts-proxy/worker.js)
- Use
wrangler secret put for production keys, or hardcode for dev
Frontend-provided mode (current default):
- Frontend sends `Authorization: Bearer *** header with user's API key
- localStorage config
{endpoint, apiKey, model} with settings gear button + preset dropdown + input fields
- Each visitor configures their own API key (or uses site proxy if unconfigured)
- Full BYOK form pattern:
references/byok-config-form.md
Switching from frontend โ server-side:
- Add env vars to Worker (
wrangler secret put API_KEY)
- Worker reads from
env instead of request headers
- Remove
X-API-Key header from frontend fetch
- Remove settings panel + localStorage config from JS
- Remove
configured conditional โ always show chat UI
Pitfalls:
- When asked "does the user need to fill in their own key?" โ always READ the code first, don't assume based on the proxy name. The Worker may or may not hold the key server-side.
- ่ฎฏ้ฃ MaaS uses
wss:// WebSocket โ must use http:// endpoint instead
- ่ฎฏ้ฃ API Key format:
appid:secret (colon-separated)
- Config not saving โ check localStorage is available (not in incognito with restrictions)
- "Failed to fetch" after config โ config wasn't saved; inject via console to test
- NEVER hardcode API keys in Worker source files that are tracked by git โ use
wrangler secret put instead. If keys are already in git, immediately:
- Rewrite Worker to read from
env (Cloudflare env vars)
wrangler secret put API_KEY to set the secret server-side
git rm --cached deploy/cloudflare/*/worker.js to stop tracking
- Add
deploy/cloudflare/*/worker.js to .gitignore
wrangler deploy to redeploy with env-based secrets
git add .gitignore && git commit -m "remove worker secrets from git"
User Preference: Release Before Risky Changes
Rule: Before implementing a new feature (e.g., AI chat panel), always create a git tag + GitHub release first so the user can roll back if the implementation breaks something.
git tag -a v1.2.1 -m "stable snapshot before X feature"
git push origin v1.2.1
gh release create v1.2.1 --title "v1.2.1: stable snapshot" --notes "..."
User explicitly requested this: "ๅ
ๆไธไธช Tag๏ผ้่ฟplaywrightๆต่ฏ้่ฟๅๅๅธdocker๏ผgithub-pagesๅcloudflare".
User Preference: Don't Dismiss Architectural Complexity
When user asks "can A and B be combined?", don't dismiss option B as "่ฟๅบฆ่ฎพ่ฎก" (over-engineering). Re-evaluate based on the actual use case:
- 4000+ article knowledge base โ RAG adds real value (cross-article discovery)
- Single-document Q&A โ RAG is overkill
- Always assess the scale before dismissing
User correction: "ไผไธ็ฅ่ฏๅบ (้RAG) ไธบไปไนๆฏ่ฟๅบฆ่ฎพ่ฎก" โ RAG was actually appropriate for their 4000-article wiki-book.
MkDocs JS Initialization Timing
MkDocs Material loads JavaScript asynchronously. Custom JS that interacts with the DOM must wait for initialization:
// WRONG โ DOM not ready:
init();
// RIGHT โ wait for MkDocs to finish:
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
setTimeout(init, 100); // MkDocs needs ~100ms after DOMContentLoaded
}
Playwright pitfall: After browser_navigate, custom JS elements (like a chat trigger button) may not exist in the DOM yet. Always wait 2-3 seconds before querying:
// Wait for custom JS to initialize
new Promise(function(resolve) { setTimeout(resolve, 3000); })
.then(function() {
var trigger = document.querySelector('.ai-chat-trigger');
// now trigger exists
});
Floating Chat Panel Pattern (Not Sidebar)
User preference: Sidebar chat panels are "ๅคช็ชไบ๏ผ้
่ฏป่ตทๆฅๆๆๅคชๅทฎ" (too narrow, poor reading experience). Use a fixed-position floating panel instead:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ [๐ค] โ fixed bottom-right button โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Talk to AI โ โ โ panel โ
โ โ โ 420px โ
โ โ [chat messages] โ wide โ
โ โ โ โ
โ โ [input field] [โ] โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
CSS (default left side โ user preference):
.ai-chat-trigger {
position: fixed; bottom: 1.5rem; left: 1.5rem;
width: 3rem; height: 3rem; border-radius: 50%;
z-index: 100;
}
.ai-chat-panel {
position: fixed; bottom: 5.5rem; left: 1.5rem;
width: 420px; max-width: calc(100vw - 2rem);
height: 560px; max-height: calc(100vh - 8rem);
z-index: 99; display: none;
}
.ai-chat-panel.open { display: flex; }
Anti-pattern: Don't put chat panels inside .md-sidebar--primary โ the sidebar is ~200px wide, making text unreadable.
Resizable Panel
Add CSS resize: both to make the panel resizable from all edges and corners:
.ai-chat-panel {
resize: both;
min-width: 300px;
min-height: 200px;
}
Pitfall: Must set min-width and min-height or the panel can be dragged to zero size. The native resize handle appears in the bottom-right corner.
Draggable Panel
Use mousedown/mousemove/mouseup on the header element. Key: switch from bottom/right to left/top positioning on first drag:
header.addEventListener("mousedown", function(e) {
if (e.target.closest("[data-action]")) return; // don't intercept button clicks
isDragging = true;
var rect = panel.getBoundingClientRect();
startX = e.clientX; startY = e.clientY;
startLeft = rect.left; startTop = rect.top;
panel.style.left = startLeft + "px";
panel.style.top = startTop + "px";
panel.style.right = "auto"; // critical: detach from right/bottom
panel.style.bottom = "auto";
e.preventDefault();
});
document.addEventListener("mousemove", function(e) {
if (!isDragging) return;
var newLeft = Math.max(0, Math.min(startLeft + e.clientX - startX, window.innerWidth - panel.offsetWidth));
var newTop = Math.max(0, Math.min(startTop + e.clientY - startY, window.innerHeight - 60));
panel.style.left = newLeft + "px";
panel.style.top = newTop + "px";
});
document.addEventListener("mouseup", function() { isDragging = false; });
Pitfall: If you don't switch to left/top positioning, dragging has no effect because bottom/right anchors fight the mouse position.
Pitfall (2026-06-30): If the header contains interactive elements besides [data-action] buttons, the drag handler's mousedown will intercept clicks on them, making them non-functional. When using a settings panel (โ๏ธ gear button), the panel itself is outside the header so no exclusion needed. But if you put a <select> or other interactive element in the header, exclude it:
header.addEventListener("mousedown", function(e) {
if (e.target.closest("[data-action]")) return;
if (e.target.closest(".ai-chat__model-select")) return; // โ only if select is in header
isDragging = true;
// ...
});
Recommended: Use a โ๏ธ gear button (with data-action="settings") that toggles a settings panel BELOW the header. Model cards go in the panel, not the header. This avoids the drag interception issue entirely.
User preference (2026-06-30): User wants proper configuration forms, not minimal UIs. When asked to add model selection, a small <select> dropdown was rejected ("ไธบไปไนๆฒกๆ้
็ฝฎ็่็ฎ" โ "why is there no configuration interface"). Use full form inputs (Endpoint, API Key, Model) with a preset dropdown for quick setup. The BYOK pattern (references/byok-config-form.md) is the expected baseline for any configurable chat widget.
Default Position: Left Side
User preference: chat panel defaults to left side, not right. Change CSS:
.ai-chat-trigger { left: 1.5rem; } /* NOT right */
.ai-chat-panel { left: 1.5rem; } /* NOT right */
E2E Testing (Multi-Phase for Large Sites)
For sites with 1000+ pages, use the layered approach in references/e2e-testing-large-static-sites.md:
- Phase 1: Bulk HTTP status check (curl -sI on every page)
- Phase 2: HTML structure analysis (h1, article tag, content length, entity links)
- Phase 3: Playwright sampling per chapter (CSS loaded, sidebar, TOC, footer)
- Phases 4-6: Link traversal, custom JS features, report generation
Quick Playwright Checks (Post-Deploy)
After any deployment, verify with actual browser clicks, not curl/grep:
- Navigate to homepage โ verify content renders
- Click key nav tabs โ verify no 404s
- Click article links โ verify URL stays on correct domain/port
- Check
window.location.href for domain/port changes after each click
AI Chat Panel Playwright Testing
When the site includes a custom AI Chat panel, test with the script pattern from references/playwright-ai-chat-e2e.md:
- Trigger button visibility, panel open/close
- Settings panel (presets, endpoint, API key, model, TTS fields)
- Config save โ verify "ๅทฒไฟๅญ โ" status
- Send message โ wait for streaming response (non-empty bubble, no typing indicator)
- Drag handle, clear chat, close panel
Pitfall: Playwright's localStorage is cleared between navigations. Verify config save via status message, not reload.
Multi-Environment Isolation
Changes to docs/ source affect ALL environments:
- Docker: needs
mkdocs build inside container + restart
- CF Pages: needs local
mkdocs build โ site/ โ wrangler deploy
- GH Pages:
git push triggers GitHub Actions build
After editing docs/, ALL THREE must be rebuilt. Never assume one deploy covers all.
Docker vs Local site/ Divergence
After docker compose up -d --build, Docker's content may differ from local site/:
docker exec <container> ls /usr/share/nginx/html/ | head -5
Verify from the container, not from local site/ directory.
OG Meta & Favicon Configuration
theme:
icon:
logo: material/book-open-page-variant
favicon: assets/images/favicon.png # Place in docs/assets/images/
extra:
social:
- icon: fontawesome/brands/github
link: https://github.com/your/repo
generator: false # Hide "Made with MkDocs"
For custom OG tags, add via extra.meta in mkdocs.yml or inject in main.html override.
References Index Generation
Generate a references page from wiki raw articles:
# scripts/generate-references.py
# Extracts title + source_url from raw/articles/*.md frontmatter
# Groups by domain, generates docs/references.md
# Filters invalid URLs (N/A, unknown, local paths)
Run: python3 scripts/generate-references.py โ adds to nav as ๅ่ๆ็ฎ: references.md
See references/batch-markdown-fixes.md for common content remediation:
- Duplicate h1 โ h2 downgrade
- HTML residuals from scraped content
- Dual
styles.css sync pitfall
MkDocs Component Reference Files
| Reference | Description |
|---|
references/mkdocs-link-fixing-patterns.md | Fixing GitHub raw URLs, relative links, double-slash, plain-text items in link sections |
references/e2e-testing-large-static-sites.md | Full 3-phase E2E methodology for 1000+ page sites |
references/batch-markdown-fixes.md | h1โh2 downgrade, HTML residual cleanup, dual styles.css sync |
references/r2-search-index.md | R2 integration for large search indexes |
references/css-customization-pitfalls.md | CSS specificity, !important requirements, sidebar/footer/dark mode |
references/video-export-via-playwright.md | Video export using app-as-renderer pattern |
references/duplicate-files-in-compiled-outputs.md | Flat/nested duplicates and slug variants |
references/escaped-characters-in-articles.md | Fixing \\n \\t \\" in ingested content |
references/wiki-book-content-remediation.md | Batch content fixes, E2E testing methodology |
references/playwright-ai-chat-e2e.md | Playwright E2E test script for AI Chat panel |
references/ai-chat-cors-proxy.md | CORS proxy Worker for LLM API integration |
references/byok-config-form.md | BYOK config form pattern (endpoint + API key + model) |
references/prev-next-entity-navigation.md | Custom JS prev/next nav for entity pages; URL regex patterns for both flat and subdirectory formats |