Skip to content

Commit e7f1157

Browse files
authored
feat: add products and breadcrumb support (#321)
1 parent 02eb72c commit e7f1157

17 files changed

Lines changed: 423 additions & 45 deletions

File tree

.changeset/dull-canyons-feel.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@alauda/doom": minor
3+
---
4+
5+
feat: add products and breadcrumb support

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ node_modules
1414
/test.*
1515
*.log
1616
*.tsbuildinfo
17+
build-info.yaml
1718

1819
# Local AI code agent configs
1920
.codex

packages/doom/src/cli/translate.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ export const translate = async ({
269269
sourceLang,
270270
targetLang,
271271
userPrompt,
272-
additionalPrompts: additionalPrompts,
272+
additionalPrompts,
273273
terms,
274274
titleTranslationPrompt,
275275
},

packages/doom/src/plugins/global/index.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,26 +74,41 @@ export const globalPlugin = ({
7474
}
7575
},
7676
addPages(config) {
77-
let loginPath: string
77+
let loginPath: string | undefined
78+
let productsPath: string | undefined
7879
for (const ext of ['.js', '.tsx']) {
7980
loginPath = baseResolve(`login/index${ext}`)
80-
if (fs.existsSync(loginPath)) {
81+
productsPath = baseResolve(`products/index${ext}`)
82+
if (fs.existsSync(loginPath) && fs.existsSync(productsPath)) {
8183
break
8284
}
8385
}
8486
if ((config.themeConfig?.locales?.length ?? 0) < 1) {
8587
return [
8688
{
8789
routePath: '/login',
88-
filepath: loginPath!,
90+
filepath: loginPath,
91+
},
92+
{
93+
routePath: '/products',
94+
filepath: productsPath,
8995
},
9096
]
9197
}
9298
const lang = config.lang
93-
return config.themeConfig!.locales!.map((l) => ({
94-
routePath: l.lang && l.lang !== lang ? `/${l.lang}/login` : '/login',
95-
filepath: loginPath,
96-
}))
99+
return config.themeConfig!.locales!.flatMap((l) => {
100+
const prefix = l.lang && l.lang !== lang ? `/${l.lang}` : ''
101+
return [
102+
{
103+
routePath: `${prefix}/login`,
104+
filepath: loginPath,
105+
},
106+
{
107+
routePath: `${prefix}/products`,
108+
filepath: productsPath,
109+
},
110+
]
111+
})
97112
},
98113
}
99114
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { addLeadingSlash, NoSSR, useSite } from '@rspress/core/runtime'
2+
import virtual from 'doom-@global-virtual'
3+
import { use } from 'react'
4+
5+
import { BuildInfoContext } from '../shared/context.ts'
6+
import { isUnversioned } from '../shared/helpers.js'
7+
8+
import { useIsPrint, useLang, useTranslation } from '@alauda/doom/runtime'
9+
import classes from '@alauda/doom/styles/products.module.scss'
10+
11+
export interface BuildInfoItem {
12+
base: string
13+
version: string
14+
displayName?: {
15+
en: string
16+
zh?: string
17+
ru?: string
18+
}
19+
}
20+
21+
export interface BuildInfoGroup {
22+
id: string
23+
items: BuildInfoItem[]
24+
}
25+
26+
const Products = () => {
27+
const lang = useLang()
28+
29+
const isPrint = useIsPrint()
30+
31+
const { site } = useSite()
32+
33+
const { groups: buildInfoGroups } = use(BuildInfoContext)
34+
35+
return (
36+
<div className={classes.container}>
37+
{buildInfoGroups.map((group) => (
38+
<div key={group.id} className={classes.group}>
39+
<h2>{group.id.toUpperCase()}</h2>
40+
<ul>
41+
{group.items.map((item) => (
42+
<li key={item.base}>
43+
<a
44+
className="rp-link"
45+
href={
46+
(isPrint ? 'https://docs.alauda.io' : '') +
47+
(virtual.prefix || '') +
48+
addLeadingSlash(item.base) +
49+
(isUnversioned(virtual.version) ? '' : `/${item.version}`) +
50+
(lang !== site.lang ? addLeadingSlash(lang) : '')
51+
}
52+
target="_blank"
53+
rel="noopener noreferrer"
54+
>
55+
{item.displayName?.[lang] ||
56+
item.displayName?.en ||
57+
item.base}
58+
</a>
59+
</li>
60+
))}
61+
</ul>
62+
</div>
63+
))}
64+
</div>
65+
)
66+
}
67+
68+
export default () => {
69+
const t = useTranslation()
70+
return (
71+
<>
72+
<h1>{t('all_product_documentation')}</h1>
73+
<p>{t('explore_doc_all_products')}</p>
74+
<NoSSR>
75+
<Products />
76+
</NoSSR>
77+
</>
78+
)
79+
}

packages/doom/src/runtime/components/ExternalSiteLink.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useLang, useSite } from '@rspress/core/runtime'
1+
import { useSite } from '@rspress/core/runtime'
22
import {
33
addTrailingSlash,
44
isExternalUrl,
@@ -14,6 +14,8 @@ import { type AnchorHTMLAttributes, type ReactNode, useMemo } from 'react'
1414
import { isUnversioned } from '../../shared/helpers.js'
1515
import { useIsPrint } from '../hooks/index.js'
1616

17+
import { useLang } from '@alauda/doom/runtime'
18+
1719
export interface ExternalSiteLinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
1820
name: string
1921
children: ReactNode
@@ -65,7 +67,7 @@ const ExternalSiteLink_ = ({
6567
(isUnversioned(virtual.version)
6668
? site.base
6769
: addTrailingSlash(site.base + site.version)) +
68-
(lang && lang !== siteData.lang ? addTrailingSlash(lang) : '') +
70+
(lang !== siteData.lang ? addTrailingSlash(lang) : '') +
6971
(hash ? `${url}#${hash}` : url)
7072
}
7173
target="_blank"

packages/doom/src/runtime/hooks/useSiteOverrides.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { isProduction, withBase } from '@rspress/core/runtime'
22
import virtual from 'doom-@global-virtual'
33
import { merge } from 'es-toolkit/compat'
44
import { useEffect, useMemo, useState } from 'react'
5+
import { xfetch } from 'x-fetch'
56
import { parse } from 'yaml'
67

78
import { isUnversioned, type Language } from '../../shared/index.js'
@@ -73,12 +74,8 @@ const fetchSiteOverrides = async (
7374
if (!url) {
7475
return
7576
}
76-
const res = await fetch(url)
77-
if (!res.ok) {
78-
return
79-
}
8077
try {
81-
return parse(await res.text()) as SiteOverrides
78+
return parse(await xfetch(url, { type: 'text' })) as SiteOverrides
8279
} catch {
8380
//
8481
}

packages/doom/src/runtime/translation.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ const en = {
6060
new_chat: 'New Chat',
6161
show_more: 'Show more',
6262
show_less: 'Show less',
63+
products: 'Products',
64+
all_product_documentation: 'All Product Documentation',
65+
explore_doc_all_products: 'Explore and find documentation for all products',
6366
}
6467

6568
export type Translation = typeof en
@@ -121,6 +124,9 @@ const zh: Translation = {
121124
new_chat: '新会话',
122125
show_more: '显示更多',
123126
show_less: '显示更少',
127+
products: '产品',
128+
all_product_documentation: '所有产品文档',
129+
explore_doc_all_products: '探索并查找所有产品的文档',
124130
}
125131

126132
const ru: Translation = {
@@ -185,6 +191,10 @@ const ru: Translation = {
185191
new_chat: 'Новый чат',
186192
show_more: 'Показать больше',
187193
show_less: 'Показать меньше',
194+
products: 'Продукты',
195+
all_product_documentation: 'Документация по всем продуктам',
196+
explore_doc_all_products:
197+
'Исследуйте и найдите документацию для всех продуктов',
188198
}
189199

190200
export const TRANSLATIONS = { en, zh, ru }
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { createContext } from 'react'
2+
3+
import type { BuildInfoGroup } from '../products/index.tsx'
4+
5+
export const BuildInfoContext = createContext<{
6+
groups: BuildInfoGroup[]
7+
setGroups: (items: BuildInfoGroup[]) => void
8+
}>(null!)
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import {
2+
addTrailingSlash,
3+
isActive,
4+
useLocation,
5+
useSidebar,
6+
useSite,
7+
} from '@rspress/core/runtime'
8+
import { IconArrowRight, SvgWrapper } from '@rspress/core/theme'
9+
import type { NormalizedSidebarGroup, SidebarData } from '@rspress/shared'
10+
import virtual from 'doom-@global-virtual'
11+
import { use, useEffect, useMemo } from 'react'
12+
import { xfetch } from 'x-fetch'
13+
import { parse } from 'yaml'
14+
15+
import type { BuildInfoGroup, BuildInfoItem } from '../../products/index.tsx'
16+
import { BuildInfoContext } from '../../shared/context.ts'
17+
import { X } from '../_X.ts'
18+
19+
import { useLang, useSiteOverrides, useTranslation } from '@alauda/doom/runtime'
20+
21+
export interface BreadcrumbItem {
22+
text: string
23+
link?: string
24+
}
25+
26+
const isBuildInfoItem = (obj: object): obj is BuildInfoItem =>
27+
'base' in obj && 'version' in obj
28+
29+
export const BreadCrumb = () => {
30+
const lang = useLang()
31+
32+
const t = useTranslation()
33+
34+
const { pathname } = useLocation()
35+
36+
const sidebar = useSidebar()
37+
38+
const site = useSiteOverrides()
39+
40+
const { site: siteData } = useSite()
41+
42+
const breadcrumbItems = useMemo(() => {
43+
function walk(
44+
sidebarItems: SidebarData,
45+
parents: NormalizedSidebarGroup[] = [],
46+
): BreadcrumbItem[] | undefined {
47+
for (const sidebarItem of sidebarItems) {
48+
if (
49+
'link' in sidebarItem &&
50+
sidebarItem.link &&
51+
isActive(sidebarItem.link, pathname)
52+
) {
53+
return [
54+
...parents.map((p) => ({
55+
text: p.text,
56+
link: p.link,
57+
})),
58+
{
59+
text: sidebarItem.text,
60+
},
61+
]
62+
}
63+
if ('items' in sidebarItem && sidebarItem.items.length) {
64+
const found = walk(sidebarItem.items, [...parents, sidebarItem])
65+
if (found) {
66+
return found
67+
}
68+
}
69+
}
70+
}
71+
return walk(sidebar)
72+
}, [pathname, sidebar])
73+
74+
const prefix = addTrailingSlash(`/${lang === siteData.lang ? '' : lang}`)
75+
76+
const { groups, setGroups: setBuildInfoGroups } = use(BuildInfoContext)
77+
78+
const hasGroups = !!groups.length
79+
80+
useEffect(() => {
81+
const fetchBuildInfo = async () => {
82+
let rawBuildInfo: Record<
83+
string,
84+
Record<string, BuildInfoItem> | BuildInfoItem
85+
>
86+
87+
try {
88+
rawBuildInfo = parse(
89+
await xfetch((virtual.prefix || '') + '/build-info.yaml', {
90+
type: 'text',
91+
}),
92+
) as Record<string, Record<string, BuildInfoItem> | BuildInfoItem>
93+
} catch {
94+
return
95+
}
96+
97+
const buildInfoGroups: BuildInfoGroup[] = []
98+
for (const [base, items] of Object.entries(rawBuildInfo)) {
99+
const id = base
100+
.replace('alauda-build-of-', '')
101+
.replace('alauda-', '')[0]
102+
let group = buildInfoGroups.find((g) => g.id === id)
103+
if (!group) {
104+
group = { id, items: [] }
105+
buildInfoGroups.push(group)
106+
}
107+
if (isBuildInfoItem(items)) {
108+
group.items.push(items)
109+
} else {
110+
const latest = Object.values(items).at(-1)
111+
if (latest) {
112+
group.items.push(latest)
113+
}
114+
}
115+
}
116+
setBuildInfoGroups(
117+
buildInfoGroups.sort((a, b) => a.id.localeCompare(b.id)),
118+
)
119+
}
120+
121+
void fetchBuildInfo()
122+
}, [setBuildInfoGroups])
123+
124+
return (
125+
<div className="breadcrumb-container">
126+
<ul className="breadcrumb-content">
127+
{hasGroups && (
128+
<li className="breadcrumb-item rp-doc">
129+
<X.a href={prefix + 'products'}>{t('products')}</X.a>
130+
</li>
131+
)}
132+
<li className="breadcrumb-item rp-doc">
133+
{hasGroups && <SvgWrapper icon={IconArrowRight} />}
134+
<X.a href={prefix}>{site.title || siteData.title}</X.a>
135+
</li>
136+
{breadcrumbItems?.map((item, index) => (
137+
// eslint-disable-next-line @eslint-react/no-array-index-key
138+
<li key={index} className="breadcrumb-item rp-doc">
139+
<SvgWrapper icon={IconArrowRight} />
140+
{item.link ? (
141+
<X.a href={item.link}>{item.text}</X.a>
142+
) : (
143+
<span className="breadcrumb-item-text">{item.text}</span>
144+
)}
145+
</li>
146+
))}
147+
</ul>
148+
</div>
149+
)
150+
}
151+
152+
export default BreadCrumb

0 commit comments

Comments
 (0)