Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
- main
- release
- feat/lb
- feat/deploy_cn
- cloudflare-pages
workflow_dispatch:

Expand Down Expand Up @@ -55,3 +56,38 @@ jobs:
${{github.workspace}}/ossutil cp ./docs/.vitepress/dist oss://lb-assets/github/release/open.longbridge.com/new-docs/raw/ -u -r -j 10 -e oss-cn-hangzhou.aliyuncs.com -i ${{ secrets.FE_LB_ASSET_ACCESS_KEY_ID }} -k ${{ secrets.FE_LB_ASSET_ACCESS_KEY_SECRET }} --include "*.md" --meta "Cache-Control:no-cache#Content-type:text/markdown;charset=utf-8"
${{github.workspace}}/ossutil cp ./docs/.vitepress/dist oss://lb-assets/github/release/open.longbridge.com/new-docs/raw/ -u -r -j 10 -e oss-cn-hangzhou.aliyuncs.com -i ${{ secrets.FE_LB_ASSET_ACCESS_KEY_ID }} -k ${{ secrets.FE_LB_ASSET_ACCESS_KEY_SECRET }} --include "*.txt" --meta "Cache-Control:no-cache#Content-type:text/plain;charset=utf-8"
${{github.workspace}}/ossutil cp ./docs/.vitepress/dist/ oss://lb-assets/github/release/open.longbridge.com/new-docs/raw/ -u -r -j 10 -e oss-cn-hangzhou.aliyuncs.com -i ${{ secrets.FE_LB_ASSET_ACCESS_KEY_ID }} -k ${{ secrets.FE_LB_ASSET_ACCESS_KEY_SECRET }} --include "*.html" --meta "Cache-Control:no-cache#Content-type:text/html;charset=utf-8"

deploy_cn:
name: build-cn
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1

- name: Download ossutils zip
run: |
wget https://github.com/aliyun/ossutil/releases/download/v1.7.17/ossutil-v1.7.17-linux-amd64.zip
unzip ossutil-v1.7.17-linux-amd64.zip && cp ./ossutil-v1.7.17-linux-amd64/ossutil ${{github.workspace}}/ && chmod +x ${{github.workspace}}/ossutil

- name: Test ossutil
run: |
${{github.workspace}}/ossutil --version

- uses: oven-sh/setup-bun@v2

- name: Install dependency
run: bun install

- name: Build CN
run: bun run build:cn

- name: Upload to Aliyun OSS (CN)
run: |
${{github.workspace}}/ossutil cp ./docs/.vitepress/dist/ oss://lb-assets/github/release/open.longbridge.cn/new-docs/raw/ -u -r -j 10 -e oss-cn-hangzhou.aliyuncs.com -i ${{ secrets.FE_LB_ASSET_ACCESS_KEY_ID }} -k ${{ secrets.FE_LB_ASSET_ACCESS_KEY_SECRET }} --exclude "*.html" --exclude "*.md" --exclude "*.txt" --exclude "*.zip" --meta=Cache-Control:max-age=31536000
${{github.workspace}}/ossutil cp ./docs/.vitepress/dist/ oss://lb-assets/github/release/open.longbridge.com/new-docs/raw/ -u -r -j 10 -e oss-cn-hangzhou.aliyuncs.com -i ${{ secrets.FE_LB_ASSET_ACCESS_KEY_ID }} -k ${{ secrets.FE_LB_ASSET_ACCESS_KEY_SECRET }} --exclude "*.html" --exclude "*.md" --exclude "*.txt" --exclude "*.zip" --meta=Cache-Control:max-age=31536000
${{github.workspace}}/ossutil cp ./docs/.vitepress/dist/ oss://lb-assets/github/release/open.longbridge.cn/new-docs/raw/ -u -r -j 10 -e oss-cn-hangzhou.aliyuncs.com -i ${{ secrets.FE_LB_ASSET_ACCESS_KEY_ID }} -k ${{ secrets.FE_LB_ASSET_ACCESS_KEY_SECRET }} --include "*.zip" --meta "Cache-Control:no-cache#Content-type:application/zip;charset=utf-8"
${{github.workspace}}/ossutil cp ./docs/.vitepress/dist oss://lb-assets/github/release/open.longbridge.cn/new-docs/raw/ -u -r -j 10 -e oss-cn-hangzhou.aliyuncs.com -i ${{ secrets.FE_LB_ASSET_ACCESS_KEY_ID }} -k ${{ secrets.FE_LB_ASSET_ACCESS_KEY_SECRET }} --include "*.md" --meta "Cache-Control:no-cache#Content-type:text/markdown;charset=utf-8"
${{github.workspace}}/ossutil cp ./docs/.vitepress/dist oss://lb-assets/github/release/open.longbridge.cn/new-docs/raw/ -u -r -j 10 -e oss-cn-hangzhou.aliyuncs.com -i ${{ secrets.FE_LB_ASSET_ACCESS_KEY_ID }} -k ${{ secrets.FE_LB_ASSET_ACCESS_KEY_SECRET }} --include "*.txt" --meta "Cache-Control:no-cache#Content-type:text/plain;charset=utf-8"
${{github.workspace}}/ossutil cp ./docs/.vitepress/dist/ oss://lb-assets/github/release/open.longbridge.cn/new-docs/raw/ -u -r -j 10 -e oss-cn-hangzhou.aliyuncs.com -i ${{ secrets.FE_LB_ASSET_ACCESS_KEY_ID }} -k ${{ secrets.FE_LB_ASSET_ACCESS_KEY_SECRET }} --include "*.html" --meta "Cache-Control:no-cache#Content-type:text/html;charset=utf-8"
41 changes: 31 additions & 10 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@ import Unocss from 'unocss/vite'
import { markdownConfig } from './config/markdown'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { mkdirSync } from 'node:fs'
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import { execSync } from 'node:child_process'
import { localesConfig } from './config/locales'
import { withMermaid } from 'vitepress-plugin-mermaid'
import { rewriteMarkdownPath } from './utils'
import { getRegionConfig, computeSrcExclude } from './region-utils'
import * as cheerio from 'cheerio'

const __dirname = dirname(fileURLToPath(import.meta.url))
const docsRoot = resolve(__dirname, '..')
const regionCfg = getRegionConfig()
const regionSrcExclude = computeSrcExclude(docsRoot)

const insertScript = (html: string) => {
const $ = cheerio.load(html)
Expand All @@ -32,11 +36,16 @@ export default defineConfig(
ignoreDeadLinks: true,
base: '/',

srcExclude: ['README.md'],
srcExclude: ['README.md', ...regionSrcExclude],
rewrites: rewriteMarkdownPath,
markdown: markdownConfig,
transformHtml(code) {
return insertScript(code)
let html = insertScript(code)
// Region URL rewriting: replace global hostname with region hostname in final HTML
if (regionCfg?.siteHostname && regionCfg.siteHostname !== 'https://open.longbridge.com') {
html = html.split('https://open.longbridge.com').join(regionCfg.siteHostname)
}
return html
},
transformHead(context) {
const { page } = context
Expand All @@ -54,13 +63,14 @@ export default defineConfig(
markdownPath = markdownPath.replace('/index.md', '.md')
}

const siteHost = regionCfg?.siteHostname || 'https://open.longbridge.com'
return [
['link', { rel: 'canonical', href: `https://open.longbridge.com/${localePathname}` }],
['link', { rel: 'alternate', hreflang: 'en', href: `https://open.longbridge.com/${pathname}` }],
['link', { rel: 'alternate', hreflang: 'zh-Hans', href: `https://open.longbridge.com/zh-CN/${pathname}` }],
['link', { rel: 'alternate', hreflang: 'zh-Hant', href: `https://open.longbridge.com/zh-HK/${pathname}` }],
['link', { rel: 'alternate', type: 'text/markdown', href: `https://open.longbridge.com/${markdownPath}` }],
['meta', { property: 'og:url', content: `https://open.longbridge.com/${localePathname}` }],
['link', { rel: 'canonical', href: `${siteHost}/${localePathname}` }],
['link', { rel: 'alternate', hreflang: 'en', href: `${siteHost}/${pathname}` }],
['link', { rel: 'alternate', hreflang: 'zh-Hans', href: `${siteHost}/zh-CN/${pathname}` }],
['link', { rel: 'alternate', hreflang: 'zh-Hant', href: `${siteHost}/zh-HK/${pathname}` }],
['link', { rel: 'alternate', type: 'text/markdown', href: `${siteHost}/${markdownPath}` }],
['meta', { property: 'og:url', content: `${siteHost}/${localePathname}` }],
['meta', { property: 'og:title', content: context.title }],
['meta', { property: 'og:description', content: context.description }],
['meta', { name: 'twitter:image', content: 'https://assets.lbctrl.com/uploads/b510b04f-9238-4fe0-b39d-11e076876ac1/longbridge-og.png' }],
Expand All @@ -75,10 +85,21 @@ export default defineConfig(
mkdirSync(outSkillDir, { recursive: true })
execSync(`zip -r "${outSkillDir}/longbridge.zip" longbridge`, { cwd: skillsDir })
console.log('✓ skill/longbridge.zip generated')

// Region URL rewriting for static assets
if (regionCfg?.siteHostname && regionCfg.siteHostname !== 'https://open.longbridge.com') {
const installDir = resolve(siteConfig.outDir, 'longbridge-terminal')
for (const file of ['install', 'install.ps1']) {
const filePath = resolve(installDir, file)
const content = readFileSync(filePath, 'utf-8')
writeFileSync(filePath, content.split('https://open.longbridge.com').join(regionCfg.siteHostname))
}
console.log('✓ install scripts rewritten for region')
}
},

sitemap: {
hostname: 'https://open.longbridge.com',
hostname: regionCfg?.siteHostname || 'https://open.longbridge.com',
transformItems(items) {
return items.filter((item) => !item.url.includes('migration'))
},
Expand Down
2 changes: 2 additions & 0 deletions docs/.vitepress/config/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { tipContainerPlugin } from '../md-plugins/tip-container'
import { GenTryItPlugin } from '../md-plugins/gen-try-it.ts'
import { NormalizeMdPlugin } from '../md-plugins/normalize-md'
import { CliCommandPlugin } from '../md-plugins/cli-command'
import { RegionFilterPlugin } from '../md-plugins/region-filter'

export const markdownConfig: MarkdownOptions = {
image: {
Expand Down Expand Up @@ -53,5 +54,6 @@ export const markdownConfig: MarkdownOptions = {
md.use(tipContainerPlugin)
md.use(GenTryItPlugin)
md.use(CliCommandPlugin)
md.use(RegionFilterPlugin)
},
}
10 changes: 7 additions & 3 deletions docs/.vitepress/locales/en/nav.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import type { DefaultTheme } from 'vitepress'
import { filterNavItems, getRegion } from '../../region-utils'

export const nav = (): DefaultTheme.NavItem[] => {
return [
const isCN = getRegion() === 'cn'
return filterNavItems([
{ text: 'Home', link: '/', activeMatch: '^(/en)?/$' },
{ text: 'Skill', link: '/skill', activeMatch: '^(/en)?/skill' },
{ text: 'Docs', link: '/docs', activeMatch: '^(/en)?/docs(?!/api)' },
isCN
? { text: 'CLI', link: '/docs/cli', activeMatch: '^(/en)?/docs/cli' }
: { text: 'Docs', link: '/docs', activeMatch: '^(/en)?/docs(?!/api)' },
{ text: 'API Reference', link: '/docs/api', activeMatch: '^(/en)?/docs/api' },
{ text: 'SDK', link: '/sdk', activeMatch: '^(/en)?/sdk' },
{ text: 'Issues', link: 'https://github.com/longbridge/openapi/issues', target: '_blank' },
]
])
}
10 changes: 7 additions & 3 deletions docs/.vitepress/locales/zh-CN/nav.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import type { DefaultTheme } from 'vitepress'
import { filterNavItems, getRegion } from '../../region-utils'

export const nav = (lang: string): DefaultTheme.NavItem[] => {
return [
const isCN = getRegion() === 'cn'
return filterNavItems([
{ text: '首页', link: `/${lang}/`, activeMatch: `^/${lang}/$` },
{ text: 'Skill', link: `/${lang}/skill`, activeMatch: `^/${lang}/skill` },
{ text: '文档', link: `/${lang}/docs`, activeMatch: `^/${lang}/docs(?!/api)` },
isCN
? { text: 'CLI', link: `/${lang}/docs/cli`, activeMatch: `^/${lang}/docs/cli` }
: { text: '文档', link: `/${lang}/docs`, activeMatch: `^/${lang}/docs(?!/api)` },
{ text: 'API 参考', link: `/${lang}/docs/api`, activeMatch: `^/${lang}/docs/api` },
{ text: 'SDK', link: `/${lang}/sdk`, activeMatch: `^/${lang}/sdk` },
{ text: 'Issues', link: 'https://github.com/longbridge/openapi/issues', target: '_blank' },
]
])
}
10 changes: 7 additions & 3 deletions docs/.vitepress/locales/zh-HK/nav.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import type { DefaultTheme } from 'vitepress'
import { filterNavItems, getRegion } from '../../region-utils'

export const nav = (lang: string): DefaultTheme.NavItem[] => {
return [
const isCN = getRegion() === 'cn'
return filterNavItems([
{ text: '首頁', link: `/${lang}/`, activeMatch: `^/${lang}/$` },
{ text: 'Skill', link: `/${lang}/skill`, activeMatch: `^/${lang}/skill` },
{ text: '文檔', link: `/${lang}/docs`, activeMatch: `^/${lang}/docs(?!/api)` },
isCN
? { text: 'CLI', link: `/${lang}/docs/cli`, activeMatch: `^/${lang}/docs/cli` }
: { text: '文檔', link: `/${lang}/docs`, activeMatch: `^/${lang}/docs(?!/api)` },
{ text: 'API 參考', link: `/${lang}/docs/api`, activeMatch: `^/${lang}/docs/api` },
{ text: 'SDK', link: `/${lang}/sdk`, activeMatch: `^/${lang}/sdk` },
{ text: 'Issues', link: 'https://github.com/longbridge/openapi/issues', target: '_blank' },
]
])
}
102 changes: 102 additions & 0 deletions docs/.vitepress/md-plugins/region-filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import type MarkdownItAsync from 'markdown-it'
import type Token from 'markdown-it/lib/token.mjs'
import picomatch from 'picomatch'
import { getRegionConfig } from '../region-utils'

/**
* Markdown-it plugin that removes sections (heading + content) based on region config.
* A "section" is defined as a heading token and all tokens until the next heading of the same or higher level.
*/
export function RegionFilterPlugin(md: MarkdownItAsync) {
const config = getRegionConfig()
if (!config) return

// URL rewriting: replace global hostname with region hostname in all inline tokens
if (config.siteHostname && config.siteHostname !== 'https://open.longbridge.com') {
md.core.ruler.push('region_url_rewrite', (state) => {
for (const token of state.tokens) {
rewriteTokenUrls(token, 'https://open.longbridge.com', config.siteHostname)
if (token.children) {
for (const child of token.children) {
rewriteTokenUrls(child, 'https://open.longbridge.com', config.siteHostname)
}
}
}
})
}

if (config.excludeSections.length === 0) return

md.core.ruler.push('region_filter', (state) => {
const env = state.env
// VitePress passes the relative file path in env.relativePath (e.g. 'en/docs/getting-started.md')
const relativePath: string | undefined = env?.relativePath
if (!relativePath) return

// Find matching exclusion rules for this page
const matchingRules = config.excludeSections.filter((rule) => picomatch(rule.page)(relativePath))
if (matchingRules.length === 0) return

// Collect all headings to exclude
const headingsToExclude = new Set<string>()
for (const rule of matchingRules) {
for (const h of rule.headings) {
headingsToExclude.add(h)
}
}

// Filter tokens: remove heading + its content section
const tokens = state.tokens
const filtered: Token[] = []
let skipUntilLevel = -1

for (let i = 0; i < tokens.length; i++) {
const token = tokens[i]

if (token.type === 'heading_open') {
const level = parseInt(token.tag.slice(1)) // h2 -> 2
// If we're skipping and hit a same-or-higher level heading, stop skipping
if (skipUntilLevel > 0 && level <= skipUntilLevel) {
skipUntilLevel = -1
}

// Check if the next inline token's content matches an excluded heading
const inlineToken = tokens[i + 1]
if (inlineToken?.type === 'inline' && headingsToExclude.has(inlineToken.content.trim())) {
skipUntilLevel = level
continue
}
}

if (skipUntilLevel > 0) {
// Check if this is a heading that should end the skip
if (token.type === 'heading_open') {
const level = parseInt(token.tag.slice(1))
if (level <= skipUntilLevel) {
skipUntilLevel = -1
filtered.push(token)
}
}
continue
}

filtered.push(token)
}

state.tokens = filtered
})
}

function rewriteTokenUrls(token: Token, from: string, to: string) {
// Handle all token types that may contain URLs: inline text, code blocks, fences, html blocks
if (token.content && token.content.includes(from)) {
token.content = token.content.split(from).join(to)
}
if (token.attrs) {
for (const attr of token.attrs) {
if (typeof attr[1] === 'string' && attr[1].includes(from)) {
attr[1] = attr[1].split(from).join(to)
}
}
}
}
89 changes: 89 additions & 0 deletions docs/.vitepress/region-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import picomatch from 'picomatch'
import fs from 'node:fs'
import path from 'node:path'
import { regionConfig, type RegionConfig } from '../../region.config'

/** Current region from VITE_REGION env, undefined means global (no filtering) */
export function getRegion(): string | undefined {
return process.env['VITE_REGION'] || undefined
}

/** Get region config, returns undefined for global build */
export function getRegionConfig(): RegionConfig | undefined {
const region = getRegion()
if (!region) return undefined
return regionConfig[region]
}

/**
* Check if a page path is included in the current region's whitelist.
* When no region is set (global build), all pages are included.
*
* @param pagePath - Relative path from docs/ root, e.g. 'en/docs/quote/pull.md'
*/
export function isPageIncluded(pagePath: string): boolean {
const config = getRegionConfig()
if (!config) return true // global build: include everything

return config.includePages.some((pattern) => picomatch(pattern)(pagePath))
}

/**
* Filter nav items based on region config.
* Removes nav items whose link matches excludeNavLinks.
* For locale-prefixed links (e.g. /zh-CN/sdk), matches against the path without locale prefix.
*/
export function filterNavItems<T extends { link?: string }>(items: T[]): T[] {
const config = getRegionConfig()
if (!config || config.excludeNavLinks.length === 0) return items

const excluded = new Set(config.excludeNavLinks)
return items.filter((item) => {
if (!item.link) return true
// Strip locale prefix for matching: /zh-CN/sdk -> /sdk, /zh-HK/docs/api -> /docs/api
const normalized = item.link.replace(/^\/(zh-CN|zh-HK)/, '')
return !excluded.has(normalized)
})
}

/**
* Compute srcExclude patterns for VitePress config.
* Scans docs directory and returns paths not matching the whitelist.
*/
export function computeSrcExclude(docsRoot: string): string[] {
const config = getRegionConfig()
if (!config) return []

const excludes: string[] = []
const locales = ['en', 'zh-CN', 'zh-HK']

for (const locale of locales) {
const localeDir = path.join(docsRoot, locale)
if (!fs.existsSync(localeDir)) continue
walkMarkdownFiles(localeDir, docsRoot, config.includePages, excludes)
}

return excludes
}

function walkMarkdownFiles(
dir: string,
docsRoot: string,
includePatterns: string[],
excludes: string[]
): void {
const entries = fs.readdirSync(dir, { withFileTypes: true })
const isMatch = picomatch(includePatterns)

for (const entry of entries) {
const fullPath = path.join(dir, entry.name)
if (entry.isDirectory()) {
walkMarkdownFiles(fullPath, docsRoot, includePatterns, excludes)
} else if (entry.name.endsWith('.md')) {
const relativePath = path.relative(docsRoot, fullPath)
if (!isMatch(relativePath)) {
excludes.push(relativePath)
}
}
}
}
Loading
Loading