fix(docs): cross ref

This commit is contained in:
chenos
2026-06-11 13:16:29 +08:00
parent 26a64a79e2
commit f8a8835600
3 changed files with 157 additions and 37 deletions
+45 -7
View File
@@ -8,6 +8,7 @@ import type {
SidebarItem,
SidebarSectionHeader,
} from '@rspress/core';
import { encodeTargetBlankHref } from '../shared/blankTargetLink';
// ── 内部类型 ──────────────────────────────────────────────────────────
@@ -29,6 +30,23 @@ function shouldProxyLink(obj: Record<string, unknown>): boolean {
return obj.proxy !== false;
}
function isExternalLink(link: string): boolean {
return link.startsWith('http://') || link.startsWith('https://');
}
function resolveSidebarLink(
obj: Record<string, unknown>,
link: string,
crossRefs: CrossRef[],
topDir: string,
): string {
if (!shouldProxyLink(obj)) {
return isExternalLink(link) ? link : encodeTargetBlankHref(link);
}
return rewriteIfCrossRef(link, crossRefs, topDir);
}
function collectLeafLinks(entries: unknown[]): string[] {
const links: string[] = [];
for (const entry of entries) {
@@ -47,6 +65,23 @@ function collectLeafLinks(entries: unknown[]): string[] {
return links;
}
function hasUnproxiedLinks(entries: unknown[]): boolean {
for (const entry of entries) {
const obj = entry as Record<string, unknown>;
if (!obj || typeof obj !== 'object') continue;
if (typeof obj.link === 'string' && !shouldProxyLink(obj)) {
return true;
}
if (Array.isArray(obj.items) && hasUnproxiedLinks(obj.items as unknown[])) {
return true;
}
}
return false;
}
/** 从相对路径取顶层模块名:'/api/auth/sub' → '/api' */
function getTopLevelDir(relDir: string): string {
const parts = relDir.split('/').filter(Boolean);
@@ -166,9 +201,12 @@ function resolveEntries(
collapsed: obj.collapsed === true,
};
if (typeof obj.link === 'string') {
group.link = shouldProxyLink(obj)
? rewriteIfCrossRef(obj.link as string, crossRefs, topDir)
: (obj.link as string);
group.link = resolveSidebarLink(
obj,
obj.link as string,
crossRefs,
topDir,
);
}
result.push(group);
continue;
@@ -178,9 +216,7 @@ function resolveEntries(
if (type === 'custom-link' && typeof obj.link === 'string') {
result.push({
text: (obj.label as string) || '',
link: shouldProxyLink(obj)
? rewriteIfCrossRef(obj.link as string, crossRefs, topDir)
: (obj.link as string),
link: resolveSidebarLink(obj, obj.link as string, crossRefs, topDir),
});
continue;
}
@@ -270,6 +306,7 @@ export function pluginCrossRefSidebar(): RspressPlugin {
config(config) {
const root = config.root || path.join(process.cwd(), 'docs');
let hasTargetBlankLinks = false;
crossRefs = [];
// 1. 扫描所有 _meta.json 检测跨模块引用(以顶层目录为单位判断)
@@ -284,6 +321,7 @@ export function pluginCrossRefSidebar(): RspressPlugin {
const meta: unknown = JSON.parse(fs.readFileSync(metaFile, 'utf-8'));
if (!Array.isArray(meta) || meta.length === 0) continue;
hasTargetBlankLinks = hasTargetBlankLinks || hasUnproxiedLinks(meta);
for (const link of collectLeafLinks(meta)) {
if (link.startsWith('http://') || link.startsWith('https://'))
@@ -311,7 +349,7 @@ export function pluginCrossRefSidebar(): RspressPlugin {
crossRefCanonicalMap[ref.aliasRoute] = ref.targetLink.split('#')[0];
}
if (crossRefs.length === 0) return config;
if (crossRefs.length === 0 && !hasTargetBlankLinks) return config;
// 2. 为所有顶层目录生成 sidebar(设置后 _meta.json 不再生效)
const sidebar: Sidebar = (config.themeConfig?.sidebar as Sidebar) || {};
+19
View File
@@ -0,0 +1,19 @@
export const TARGET_BLANK_PREFIX = '__target_blank__:';
export function encodeTargetBlankHref(href: string): string {
return `${TARGET_BLANK_PREFIX}${href}`;
}
export function decodeTargetBlankHref(href: string) {
if (!href.startsWith(TARGET_BLANK_PREFIX)) {
return {
href,
targetBlank: false,
};
}
return {
href: href.slice(TARGET_BLANK_PREFIX.length) || '/',
targetBlank: true,
};
}
+93 -30
View File
@@ -1,12 +1,30 @@
import { NoSSR, useFrontmatter, useLang, useNavigate, usePage, usePages } from '@rspress/core/runtime';
import {
NoSSR,
useFrontmatter,
useLang,
useNavigate,
usePage,
usePages,
} from '@rspress/core/runtime';
import type { Feature } from '@rspress/core';
import { Badge, DocContent, DocLayout as OriginalDocLayout, getCustomMDXComponent as basicGetCustomMDXComponent, Layout as BasicLayout, HomeFooter, Link, renderHtmlOrText, Tab, Tabs } from '@rspress/core/theme-original';
import {
Badge,
DocContent,
DocLayout as OriginalDocLayout,
getCustomMDXComponent as basicGetCustomMDXComponent,
Layout as BasicLayout,
HomeFooter,
Link as OriginalLink,
renderHtmlOrText,
Tab,
Tabs,
} from '@rspress/core/theme-original';
import {
LlmsContainer,
LlmsCopyButton,
LlmsViewOptions,
} from '@rspress/plugin-llms/runtime';
import type { ComponentProps, JSX, ReactNode } from 'react';
import React, { type ComponentProps, type JSX, type ReactNode } from 'react';
import { PluginCard } from './components/PluginCard';
import { PluginInfo } from './components/PluginInfo';
import { PluginList } from './components/PluginList';
@@ -14,6 +32,7 @@ import { ProvidedBy } from './components/ProvidedBy';
import './index.scss';
import { transformHref, useLangPrefix } from './utils';
import { HomeHero } from './components/HomeHero';
import { decodeTargetBlankHref } from '../shared/blankTargetLink';
function getCustomMDXComponent() {
const { h1: H1, ...mdxComponents } = basicGetCustomMDXComponent();
@@ -46,6 +65,24 @@ function getCustomMDXComponent() {
export { getCustomMDXComponent };
export function Link(props: ComponentProps<typeof OriginalLink>) {
const { href = '/', rel, target, ...restProps } = props;
const { href: nextHref, targetBlank } = decodeTargetBlankHref(href);
if (!targetBlank) {
return <OriginalLink {...props} />;
}
return (
<OriginalLink
{...restProps}
href={nextHref}
target={target ?? '_blank'}
rel={rel ?? 'noopener noreferrer'}
/>
);
}
export interface HomeLayoutProps {
beforeHero?: ReactNode;
afterHero?: ReactNode;
@@ -78,15 +115,20 @@ type ThemePage = {
frontmatter?: ThemeFrontmatter & Record<string, unknown>;
};
function getFeatureGroups(page?: ThemePage | { frontmatter?: ThemeFrontmatter }): HomeFeatureGroup[] {
function getFeatureGroups(
page?: ThemePage | { frontmatter?: ThemeFrontmatter },
): HomeFeatureGroup[] {
return page?.frontmatter?.features ?? [];
}
function isPluginDetailPage(frontmatter?: ThemeFrontmatter, routePath?: string): boolean {
function isPluginDetailPage(
frontmatter?: ThemeFrontmatter,
routePath?: string,
): boolean {
return Boolean(
frontmatter?.displayName &&
frontmatter?.packageName &&
routePath?.includes('/plugins/@nocobase/'),
frontmatter?.packageName &&
routePath?.includes('/plugins/@nocobase/'),
);
}
@@ -117,16 +159,14 @@ export function HomeLayout(props: HomeLayoutProps) {
>
<div className="rp-pb-12">
{beforeHero}
{
frontmatter.hero && (
<HomeHero
frontmatter={frontmatter}
routePath={routePath}
beforeHeroActions={beforeHeroActions}
afterHeroActions={afterHeroActions}
/>
)
}
{frontmatter.hero && (
<HomeHero
frontmatter={frontmatter}
routePath={routePath}
beforeHeroActions={beforeHeroActions}
afterHeroActions={afterHeroActions}
/>
)}
{afterHero}
{beforeFeatures}
<HomeFeature />
@@ -168,8 +208,8 @@ export const Layout = () => {
};
const isPluginDetailPage = Boolean(
frontmatter?.displayName &&
frontmatter?.packageName &&
routePath?.startsWith('/plugins/@'),
frontmatter?.packageName &&
routePath?.startsWith('/plugins/@'),
);
const pageClassName = [
isPluginDetailPage ? 'plugin-detail-page' : '',
@@ -199,7 +239,9 @@ export const Layout = () => {
export function DocLayout(props: ComponentProps<typeof OriginalDocLayout>) {
const { page } = usePage();
const { frontmatter } = useFrontmatter() as { frontmatter?: ThemeFrontmatter };
const { frontmatter } = useFrontmatter() as {
frontmatter?: ThemeFrontmatter;
};
if (!isPluginDetailPage(frontmatter, page.routePath)) {
return <OriginalDocLayout {...props} />;
@@ -237,7 +279,9 @@ function HomeFeatureItem({ feature }: { feature: Feature }): JSX.Element {
<div className="rp-home-feature__item-wrapper">
<article
key={title}
className={`rp-home-feature__card ${link ? 'rp-home-feature__card--clickable' : ''}`}
className={`rp-home-feature__card ${
link ? 'rp-home-feature__card--clickable' : ''
}`}
onClick={() => {
if (link) {
navigate(transformHref(link, langPrefix));
@@ -252,7 +296,9 @@ function HomeFeatureItem({ feature }: { feature: Feature }): JSX.Element {
<h2 className="rp-home-feature__title">
{link ? (
<Link href={transformHref(link, langPrefix)}>{title}</Link>
) : title}
) : (
title
)}
</h2>
</div>
<p
@@ -266,7 +312,9 @@ function HomeFeatureItem({ feature }: { feature: Feature }): JSX.Element {
}
export function HomeFeature() {
const { frontmatter } = useFrontmatter() as { frontmatter?: ThemeFrontmatter };
const { frontmatter } = useFrontmatter() as {
frontmatter?: ThemeFrontmatter;
};
const { pages } = usePages() as { pages: ThemePage[] };
const lang = useLang();
const featureGroups = frontmatter?.features ?? [];
@@ -278,17 +326,32 @@ export function HomeFeature() {
let items = feature.items ?? [];
if (index === 2) {
const page = pages.find((currentPage) => currentPage.lang === lang && currentPage.frontmatter?.pageName === 'guide');
const page = pages.find(
(currentPage) =>
currentPage.lang === lang &&
currentPage.frontmatter?.pageName === 'guide',
);
if (page) {
const allItems = getFeatureGroups(page).flatMap((group) => group.items ?? []);
const allItems = getFeatureGroups(page).flatMap(
(group) => group.items ?? [],
);
items = [...allItems.filter((item) => item.showOnHome), ...items];
}
} else if (index === 3) {
const page = pages.find((currentPage) => currentPage.lang === lang && currentPage.frontmatter?.pageName === 'development');
const page = pages.find(
(currentPage) =>
currentPage.lang === lang &&
currentPage.frontmatter?.pageName === 'development',
);
if (page) {
// 把 page.frontmatter?.features 里的 items 都拍平合并,取前 8 个
const allItems = getFeatureGroups(page).flatMap((group) => group.items ?? []);
items = [...allItems.filter((item) => item.showOnHome), ...(feature.items ?? [])];
const allItems = getFeatureGroups(page).flatMap(
(group) => group.items ?? [],
);
items = [
...allItems.filter((item) => item.showOnHome),
...(feature.items ?? []),
];
}
}
@@ -304,7 +367,7 @@ export function HomeFeature() {
})}
</div>
</div>
)
);
})}
</div>
);
@@ -325,7 +388,7 @@ export function HomeFeature() {
})}
</div>
</div>
)
);
})}
</div>
);