wiki: one header on every page, English by default, one switch for 中文

Each section carried its own copy of the header: Home's nav had no Home,
the docs' nav had no Docs, the research post had no star or theme button,
and the generated Alpha Library pages had no header at all. Only the docs
could switch language, and they guessed Chinese from the browser while
every other page stayed English.

- partials/ holds one head block, header and footer; sync_site_chrome.py
  writes them into every page between markers and, with --check in CI,
  also requires <body data-section> (the current section is highlighted by
  CSS), the language attributes and a single ?v= token site-wide. The
  Alpha Library generator inserts the same partials.
- site.js is the shared shell (theme, stars, language, footer traffic on
  home only). English is the HTML; Chinese comes from locales/zh.json via
  data-i18n hooks, and check_i18n.py fails on a key with no Chinese or a
  Chinese string nothing uses. A URL that names a language (/docs/zh/,
  the two articles) shows it and remembers it; everything else follows the
  stored choice, English by default. theme-init.js keeps a page hidden until
  its Chinese text is in, so it never flashes English.
- English tutorial and Chinese research post added. The Chinese tutorial
  gets the corrections its translation surfaced: 462 alphas in five zoos,
  AAPL.US, the mandate caps trades per day (it has no daily loss limit),
  Shadow Account extracts up to five rules, the web UI needs Docker or a
  checkout.
- Alpha Library: the zoo pages were never deployed — content/ is
  gitignored and nothing built it, so every zoo link served the root
  redirect. The deploy now exports the manifest and renders the pages (and
  redeploys when agent/src/factors changes); the fifth zoo, fundamental, is
  listed; generated pages allow same-origin scripts only.
- Phones keep the nav as a scrollable row instead of hiding it.
This commit is contained in:
Haozhe Wu committed 2026-09-29 17:53:13 +08:00
1 parent 36485bf55b
commit a7f645f368
32 files changed
+2766 -717

No files matched your search

+7 -4
View File
@@ -1026,22 +1026,25 @@ vibe-trading playbook create premarket-brief</code></pre>
<li>Output directory: <code>.</code></li>
<li>Custom domain: <code>vibetrading.wiki</code></li>
</ul>
<p>A push to <code>main</code> that touches <code>wiki/</code> deploys it.</p>
<p>A push to <code>main</code> that touches <code>wiki/</code> or <code>agent/src/factors/</code> deploys it; the deploy generates the Alpha Library pages first.</p>
`
},
{
id: "editing",
title: "Editing the docs",
body: `
<p>English pages live in <code>wiki/docs/content.en.js</code>, Chinese pages in <code>wiki/docs/content.zh.js</code>. Both must have the same pages and sections in the same order; CI runs the check below. English is served at <code>/docs/latest/…</code>, Chinese at <code>/docs/zh/…</code>.</p>
<pre><code>node wiki/scripts/check_docs_parity.mjs</code></pre>
<p>English pages live in <code>wiki/docs/content.en.js</code>, Chinese pages in <code>wiki/docs/content.zh.js</code>. Both must have the same pages and sections in the same order. English is served at <code>/docs/latest/…</code>, Chinese at <code>/docs/zh/…</code>.</p>
<p>Every page of the site shares one header, footer and head block from <code>wiki/partials/</code>. The rest of the site is English HTML with Chinese in <code>wiki/locales/zh.json</code>. English is the default; the header button switches the whole site and remembers the choice. CI runs:</p>
<pre><code>node wiki/scripts/check_docs_parity.mjs
python3 wiki/scripts/sync_site_chrome.py --check
python3 wiki/scripts/check_i18n.py</code></pre>
`
},
{
id: "why-static",
title: "Why static",
body: `
<p>Docs, landing copy, redirects, theme and search all work as static files. No VPS, database or server process is needed; the only server-side code is an anonymous page-view counter.</p>
<p>Docs, landing copy, redirects, theme, language and search all work as static files. No VPS, database or server process is needed; the only server-side code is an anonymous page-view counter.</p>
`
}
]
+4 -10
View File
@@ -1,5 +1,5 @@
import { DOCS_STRUCTURE as EN_STRUCTURE } from "/docs/content.en.js?v=20260929";
import { DOCS_STRUCTURE as ZH_STRUCTURE } from "/docs/content.zh.js?v=20260929";
import { DOCS_STRUCTURE as EN_STRUCTURE } from "/docs/content.en.js?v=20260929d";
import { DOCS_STRUCTURE as ZH_STRUCTURE } from "/docs/content.zh.js?v=20260929d";
// One content set per language; every version alias renders the latest.
export const DOCS_DEFAULT_VERSION = "0.1.16";
@@ -22,30 +22,24 @@ export const DOCS_VERSIONS = [
export const DOCS_UI = {
en: {
siteTitle: "Vibe-Trading Docs",
brandSuffix: "Docs",
search: "Search",
searchPlaceholder: "Search docs",
version: "Version",
language: "Language",
onThisPage: "On this page",
previous: "Previous",
next: "Next",
noMatch: "No pages match that search.",
navLabel: "Documentation navigation",
nav: { home: "Home", tutorials: "Tutorials", alpha: "Alpha Library", lab: "Research Lab", source: "Source" }
navLabel: "Documentation navigation"
},
zh: {
siteTitle: "Vibe-Trading 文档",
brandSuffix: "文档",
search: "搜索",
searchPlaceholder: "搜索文档",
version: "版本",
language: "语言",
onThisPage: "本页内容",
previous: "上一页",
next: "下一页",
noMatch: "没有匹配的页面。",
navLabel: "文档导航",
nav: { home: "首页", tutorials: "教程", alpha: "因子库", lab: "研究室", source: "源码" }
navLabel: "文档导航"
}
};
+7 -4
View File
@@ -1025,22 +1025,25 @@ vibe-trading playbook create premarket-brief</code></pre>
<li>输出目录:<code>.</code></li>
<li>自定义域名:<code>vibetrading.wiki</code></li>
</ul>
<p>推送到 <code>main</code> 且改动了 <code>wiki/</code> 时自动部署。</p>
<p>推送到 <code>main</code> 且改动了 <code>wiki/</code> 或 <code>agent/src/factors/</code> 时自动部署;部署前会先生成因子库页面。</p>
`
},
{
id: "editing",
title: "编辑文档",
body: `
<p>英文页面在 <code>wiki/docs/content.en.js</code>,中文页面在 <code>wiki/docs/content.zh.js</code>。两者的页面和小节必须一一对应、顺序一致,CI 会运行下面的检查。英文地址是 <code>/docs/latest/…</code>,中文地址是 <code>/docs/zh/…</code>。</p>
<pre><code>node wiki/scripts/check_docs_parity.mjs</code></pre>
<p>英文页面在 <code>wiki/docs/content.en.js</code>,中文页面在 <code>wiki/docs/content.zh.js</code>。两者的页面和小节必须一一对应、顺序一致。英文地址是 <code>/docs/latest/…</code>,中文地址是 <code>/docs/zh/…</code>。</p>
<p>全站所有页面共用 <code>wiki/partials/</code> 里的同一套页头、页脚和 head 公共部分。其余页面的英文写在 HTML 里,中文在 <code>wiki/locales/zh.json</code>。默认英文,顶栏按钮切换整站语言并记住选择。CI 会运行:</p>
<pre><code>node wiki/scripts/check_docs_parity.mjs
python3 wiki/scripts/sync_site_chrome.py --check
python3 wiki/scripts/check_i18n.py</code></pre>
`
},
{
id: "why-static",
title: "为什么是静态站",
body: `
<p>文档、首页文案、跳转、主题和搜索都能以静态文件实现,不需要 VPS、数据库或常驻服务;唯一的服务端代码是一个匿名的访问计数器。</p>
<p>文档、首页文案、跳转、主题、语言和搜索都能以静态文件实现,不需要 VPS、数据库或常驻服务;唯一的服务端代码是一个匿名的访问计数器。</p>
`
}
]
+39 -29
View File
@@ -2,19 +2,6 @@
<html lang="en">
<head>
<meta charset="utf-8">
<script>
(function () {
try {
var stored = localStorage.getItem("vibetrading-theme");
var resolved = stored === "dark" || stored === "light"
? stored
: window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
document.documentElement.setAttribute("data-theme", resolved);
} catch (e) {
document.documentElement.setAttribute("data-theme", "light");
}
})();
</script>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Vibe-Trading Docs</title>
<meta name="description" content="Documentation for Vibe-Trading, the natural-language finance research agent. 中文文档见 /docs/zh/。">
@@ -29,40 +16,46 @@
<link rel="alternate" hreflang="en" href="https://vibetrading.wiki/docs/latest/getting-started/vibe-trading-overview">
<link rel="alternate" hreflang="zh-CN" href="https://vibetrading.wiki/docs/zh/getting-started/vibe-trading-overview">
<link rel="alternate" hreflang="x-default" href="https://vibetrading.wiki/docs/latest/getting-started/vibe-trading-overview">
<!-- site-head:start -->
<link rel="icon" type="image/png" href="/assets/icon.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,600;9..144,750&family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css">
<link rel="stylesheet" href="/docs/styles.css?v=20260929">
<script type="module" src="/docs/main.js?v=20260929"></script>
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,600;9..144,750&amp;family=IBM+Plex+Mono:wght@400;500;600&amp;family=IBM+Plex+Sans:wght@400;500;600;700&amp;display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css?v=20260929d">
<script src="/theme-init.js?v=20260929d"></script>
<!-- site-head:end -->
<link rel="stylesheet" href="/docs/styles.css?v=20260929d">
<script type="module" src="/docs/main.js?v=20260929d"></script>
</head>
<body class="docs-body">
<header class="site-header docs-header" id="site-header">
<body class="docs-body" data-section="docs">
<!-- site-header:start -->
<header class="site-header" id="site-header">
<div class="site-header__inner">
<a class="brand" href="/home/" aria-label="Vibe-Trading home">
<a class="brand" href="/home/" aria-label="Vibe-Trading home" data-i18n-attr="aria-label=header.homeLabel">
<img class="brand__mark" src="/assets/icon.png" alt="" width="40" height="40">
<span class="brand__text">Vibe-Trading <em data-ui="brandSuffix">Docs</em></span>
<span class="brand__text">Vibe-Trading</span>
</a>
<nav class="site-nav" aria-label="Primary">
<a href="/home/" data-ui-nav="home">Home</a>
<a href="/tutorials/" data-ui-nav="tutorials">Tutorials</a>
<a href="/alpha-library/" data-ui-nav="alpha">Alpha Library</a>
<a href="/research-lab/" data-ui-nav="lab">Research Lab</a>
<a href="https://github.com/HKUDS/Vibe-Trading" data-ui-nav="source">Source</a>
<nav class="site-nav" aria-label="Primary" data-i18n-attr="aria-label=header.navLabel">
<a href="/home/" data-nav="home" data-i18n="nav.home">Home</a>
<a href="/docs/" data-nav="docs" data-i18n="nav.docs">Docs</a>
<a href="/tutorials/" data-nav="tutorials" data-i18n="nav.tutorials">Tutorials</a>
<a href="/alpha-library/" data-nav="alpha" data-i18n="nav.alpha">Alpha Library</a>
<a href="/research-lab/" data-nav="lab" data-i18n="nav.lab">Research Lab</a>
<a href="https://github.com/HKUDS/Vibe-Trading">GitHub</a>
</nav>
<div class="site-actions">
<a class="lang-pill" id="lang-toggle" href="/docs/zh/getting-started/vibe-trading-overview" hreflang="zh-CN" lang="zh-CN" data-lang="zh">中文</a>
<button class="lang-pill" id="lang-toggle" type="button" lang="zh-CN" aria-label="切换到中文">中文</button>
<a class="star-pill" href="https://github.com/HKUDS/Vibe-Trading" aria-label="Vibe-Trading on GitHub">
<span aria-hidden="true">Star</span>
<strong id="star-count">--</strong>
</a>
<button class="icon-button" id="theme-toggle" type="button" aria-label="Toggle color theme">
<button class="icon-button" id="theme-toggle" type="button" aria-label="Toggle color theme" data-i18n-attr="aria-label=header.themeLabel">
<span class="theme-icon" aria-hidden="true"></span>
</button>
</div>
</div>
</header>
<!-- site-header:end -->
<main class="docs-shell">
<aside class="docs-sidebar" aria-label="Documentation navigation">
@@ -86,5 +79,22 @@
<nav id="docs-outline"></nav>
</aside>
</main>
<!-- site-footer:start -->
<footer class="site-footer">
<span class="site-footer__name">Vibe-Trading Wiki</span>
<a href="/docs/" data-i18n="nav.docs">Docs</a>
<a href="/tutorials/" data-i18n="nav.tutorials">Tutorials</a>
<a href="/alpha-library/" data-i18n="nav.alpha">Alpha Library</a>
<a href="/research-lab/" data-i18n="nav.lab">Research Lab</a>
<a href="https://github.com/HKUDS/Vibe-Trading">GitHub</a>
<a href="https://pypi.org/project/vibe-trading-ai/">PyPI</a>
<div class="footer-traffic" id="footer-traffic" hidden>
<span class="ft-item">🤖 <strong id="vt-agents">–</strong> <span data-i18n="footer.agents">AI agents</span></span>
<span class="ft-item">👤 <strong id="vt-humans">–</strong> <span data-i18n="footer.humans">humans</span></span>
<span class="ft-item">📦 <strong id="vt-installs">–</strong> <span data-i18n="footer.installs">installs/mo</span></span>
<span class="ft-note" data-i18n="footer.note">visitors to this site · anonymous · first-party</span>
</div>
</footer>
<!-- site-footer:end -->
</body>
</html>
+12 -109
View File
@@ -1,17 +1,12 @@
import { initTheme } from "/theme.js";
import { initSite, setPageLang, storedLang } from "/site.js?v=20260929d";
import {
DOCS_DEFAULT_LANG,
DOCS_DEFAULT_PAGE,
DOCS_LANGUAGES,
DOCS_UI,
DOCS_VERSIONS
} from "/docs/content.js?v=20260929";
} from "/docs/content.js?v=20260929d";
const REPO = "HKUDS/Vibe-Trading";
const API = `https://api.github.com/repos/${REPO}`;
const STARS_CACHE_KEY = "vibetrading-github-stars";
const STARS_TTL_MS = 12 * 60 * 60 * 1000;
const LANG_KEY = "vibetrading-docs-lang";
const SITE = "https://vibetrading.wiki";
function stripTags(html) {
@@ -36,41 +31,6 @@ const pagesByLang = Object.fromEntries(
])
);
function formatStarCount(n) {
if (typeof n !== "number" || !Number.isFinite(n) || n < 0) return "--";
if (n < 1000) return String(Math.round(n));
if (n < 1_000_000) return `${(n / 1000).toFixed(1)}k`;
return `${(n / 1_000_000).toFixed(1)}m`;
}
function initStars() {
const el = document.getElementById("star-count");
if (!el) return;
let cached = null;
try {
cached = JSON.parse(localStorage.getItem(STARS_CACHE_KEY) || "null");
} catch {
cached = null;
}
if (cached && typeof cached.count === "number") el.textContent = formatStarCount(cached.count);
if (cached && Date.now() - cached.at < STARS_TTL_MS) return;
fetch(API, { headers: { Accept: "application/vnd.github+json" } })
.then((r) => (r.ok ? r.json() : Promise.reject(new Error(String(r.status)))))
.then((data) => {
if (typeof data.stargazers_count !== "number") return;
try {
localStorage.setItem(STARS_CACHE_KEY, JSON.stringify({ count: data.stargazers_count, at: Date.now() }));
} catch {
/* ignore */
}
el.textContent = formatStarCount(data.stargazers_count);
})
.catch(() => {
if (!cached) el.textContent = "--";
});
}
function slugify(text) {
return String(text)
@@ -84,12 +44,15 @@ function langForSegment(segment) {
return match ? match[0] : DOCS_DEFAULT_LANG;
}
// The URL decides the language: /docs/zh/… is Chinese, /docs/latest/… (or an
// old version number) English. Only a URL that names no language — /docs/
// itself — falls back to the reader's stored choice, English by default.
function routeParts() {
const normalized = location.pathname.replace(/\/+$/, "");
const match = normalized.match(/^\/docs\/([^/]+)\/(.+)$/);
if (!match) {
const bare = normalized.match(/^\/docs\/([^/]+)$/);
const lang = bare ? langForSegment(bare[1]) : DOCS_DEFAULT_LANG;
const lang = bare && bare[1] !== "index.html" ? langForSegment(bare[1]) : storedLang();
return { lang, pageId: DOCS_DEFAULT_PAGE, canonical: false };
}
const lang = langForSegment(match[1]);
@@ -105,27 +68,6 @@ function resolvePage(pageId, lang) {
return pages.find((page) => page.id === pageId) || pages.find((page) => page.id === DOCS_DEFAULT_PAGE);
}
function storedLang() {
try {
const value = localStorage.getItem(LANG_KEY);
return value && value in DOCS_LANGUAGES ? value : null;
} catch {
return null;
}
}
function storeLang(lang) {
try {
localStorage.setItem(LANG_KEY, lang);
} catch {
/* ignore */
}
}
function browserLang() {
const first = (navigator.languages && navigator.languages[0]) || navigator.language || "";
return first.toLowerCase().startsWith("zh") ? "zh" : DOCS_DEFAULT_LANG;
}
function setAlternate(hreflang, href) {
let link = document.querySelector(`link[rel='alternate'][hreflang='${hreflang}']`);
@@ -156,17 +98,12 @@ function setMeta(page, lang) {
setAlternate("x-default", `${SITE}${canonicalPath(page.id, DOCS_DEFAULT_LANG)}`);
}
function applyChrome(lang, page) {
function applyChrome(lang) {
const ui = DOCS_UI[lang];
document.documentElement.lang = DOCS_LANGUAGES[lang].htmlLang;
document.querySelectorAll("[data-ui]").forEach((el) => {
const value = ui[el.getAttribute("data-ui")];
if (typeof value === "string") el.textContent = value;
});
document.querySelectorAll("[data-ui-nav]").forEach((el) => {
const value = ui.nav[el.getAttribute("data-ui-nav")];
if (typeof value === "string") el.textContent = value;
});
document.getElementById("docs-search")?.setAttribute("placeholder", ui.searchPlaceholder);
document.querySelector(".docs-sidebar")?.setAttribute("aria-label", ui.navLabel);
document.querySelector(".docs-outline")?.setAttribute("aria-label", ui.onThisPage);
@@ -177,16 +114,6 @@ function applyChrome(lang, page) {
`<option value="${item.name}">${item.label[lang] || item.name}</option>`
).join("");
}
const toggle = document.getElementById("lang-toggle");
if (toggle) {
const other = Object.keys(DOCS_LANGUAGES).find((code) => code !== lang) || DOCS_DEFAULT_LANG;
toggle.textContent = DOCS_LANGUAGES[other].label;
toggle.setAttribute("href", canonicalPath(page.id, other));
toggle.setAttribute("hreflang", DOCS_LANGUAGES[other].htmlLang);
toggle.setAttribute("lang", DOCS_LANGUAGES[other].htmlLang);
toggle.dataset.lang = other;
}
}
function navLink(page, currentId) {
@@ -277,8 +204,10 @@ function renderCurrent() {
history.replaceState({}, "", canonicalPath(page.id, lang) + location.hash);
}
current = { lang, pageId: page.id };
setPageLang(lang);
setMeta(page, lang);
applyChrome(lang, page);
applyChrome(lang);
renderNav(page.id, lang, document.getElementById("docs-search")?.value || "");
renderArticle(page, lang);
renderOutline(page);
@@ -286,23 +215,6 @@ function renderCurrent() {
document.getElementById("docs-article")?.focus({ preventScroll: true });
}
function initLanguage() {
const { lang, pageId } = routeParts();
const preferred = storedLang() || browserLang();
if (lang === DOCS_DEFAULT_LANG && preferred !== DOCS_DEFAULT_LANG) {
history.replaceState({}, "", canonicalPath(pageId, preferred) + location.hash);
}
document.getElementById("lang-toggle")?.addEventListener("click", (event) => {
const target = event.currentTarget.dataset.lang;
if (!target || !(target in DOCS_LANGUAGES)) return;
storeLang(target);
if (event.metaKey || event.ctrlKey || event.shiftKey || event.button === 1) return;
event.preventDefault();
navigate(event.currentTarget.getAttribute("href"));
});
}
function initSearch() {
const search = document.getElementById("docs-search");
if (!search) return;
@@ -313,21 +225,12 @@ function initSearch() {
});
}
function initHeaderScroll() {
const header = document.getElementById("site-header");
if (!header) return;
const sync = () => header.classList.toggle("is-scrolled", window.scrollY > 10);
sync();
window.addEventListener("scroll", sync, { passive: true });
}
let current = { lang: DOCS_DEFAULT_LANG, pageId: DOCS_DEFAULT_PAGE };
window.addEventListener("popstate", renderCurrent);
initTheme();
initStars();
initLanguage();
initSite({ onToggle: (lang) => navigate(canonicalPath(current.pageId, lang)) });
initSearch();
initHeaderScroll();
renderCurrent();
// The sections are rendered after load, so the browser could not scroll to
// a #section in the URL on its own.
+4 -27
View File
@@ -6,15 +6,6 @@
background-size: 36px 36px, 36px 36px, auto;
}
.brand__text em {
color: var(--muted);
font-family: var(--mono);
font-size: 0.72rem;
font-style: normal;
font-weight: 600;
text-transform: uppercase;
}
.docs-shell {
display: grid;
grid-template-columns: 280px minmax(0, 1fr) 210px;
@@ -277,6 +268,10 @@
max-height: none;
}
.doc-section {
scroll-margin-top: 120px;
}
.doc-footer {
grid-template-columns: 1fr;
}
@@ -286,24 +281,6 @@
}
}
.lang-pill {
display: flex;
align-items: center;
min-height: 38px;
border: 1px solid var(--line);
border-radius: 6px;
background: var(--surface);
color: var(--ink);
padding: 0 12px;
font-size: 0.86rem;
font-weight: 700;
white-space: nowrap;
}
.lang-pill:hover {
border-color: var(--accent);
}
.doc-section table {
width: 100%;
max-width: 860px;