Skip to content
Open
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
63 changes: 53 additions & 10 deletions apps/wiki/app/[language]/(documents)/[...slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ interface DocParams {
slug: string[];
}

type NavigationLinkRelation = 'sibling' | 'sequence';

interface NavigationLinkInfo {
item: DocItem;
relation: NavigationLinkRelation;
}

export async function generateStaticParams() {
// 获取语言配置
const languageConfigs = getLanguageConfigs();
Expand Down Expand Up @@ -251,6 +258,7 @@ export default async function DocPage({
root: navigationItemRoot,
map: navigationItemMap,
redirectMap,
orderMap: navigationOrderMap,
} = await getDocsNavigationMap(language, slugArray[0]);

const navItem = getDocItemByNavigationMap(navigationItemMap, slugPath);
Expand Down Expand Up @@ -399,6 +407,37 @@ export default async function DocPage({
const nextPage =
currentIndex < siblings.length - 1 ? siblings[currentIndex + 1] : null;

const navigationOrderEntry =
navigationOrderMap.get(navItem.displayPath) ?? null;
const sequentialPreviousPath = navigationOrderEntry?.previous ?? null;
const sequentialNextPath = navigationOrderEntry?.next ?? null;
const sequentialPreviousItem = sequentialPreviousPath
? getDocItemByNavigationMap(navigationItemMap, sequentialPreviousPath)
: null;
const sequentialNextItem = sequentialNextPath
? getDocItemByNavigationMap(navigationItemMap, sequentialNextPath)
: null;
const previousNav: NavigationLinkInfo | null = previousPage
? { item: previousPage, relation: 'sibling' }
: sequentialPreviousItem
? { item: sequentialPreviousItem, relation: 'sequence' }
: null;
const nextNav: NavigationLinkInfo | null = nextPage
? { item: nextPage, relation: 'sibling' }
: sequentialNextItem
? { item: sequentialNextItem, relation: 'sequence' }
: null;
const previousLabelKey = previousNav
? previousNav.relation === 'sequence'
? 'previousPageSequence'
: 'previousPageSibling'
: null;
const nextLabelKey = nextNav
? nextNav.relation === 'sequence'
? 'nextPageSequence'
: 'nextPageSibling'
: null;

const showEditAndLastModifiedTime = strippedSource.trim().length > 0;

// 获取文件的最近修改时间
Expand Down Expand Up @@ -520,39 +559,43 @@ export default async function DocPage({
</section>
)}
{/* 上一页/下一页导航 */}
{(previousPage || nextPage) && (
{(previousNav || nextNav) && (
<nav className="mt-8 flex justify-between items-center p-4 bg-base-100/30 rounded-lg border border-base-300/30 shadow-sm">
{previousPage ? (
{previousNav ? (
<Link
href={`/${language}/${previousPage.displayPath}`}
href={`/${language}/${previousNav.item.displayPath}`}
className="inline-flex items-center text-sm text-base-content/70 hover:text-primary transition-colors"
aria-label={`${t(previousLabelKey ?? 'previousPage', language)}: ${previousNav.item.metadata.title}`}
>
<ChevronLeft className="w-4 h-4 mr-2" />
<div>
<div className="text-xs text-base-content/50">
{t('previousPage', language)}
{t(previousLabelKey ?? 'previousPage', language)}
</div>
<div className="font-medium">
{previousNav.item.metadata.title}
</div>
<div className="font-medium">{previousPage.metadata.title}</div>
</div>
</Link>
) : (
<span />
)}

{nextPage && (
{nextNav ? (
<Link
href={`/${language}/${nextPage.displayPath}`}
href={`/${language}/${nextNav.item.displayPath}`}
className="inline-flex items-center text-sm text-base-content/70 hover:text-primary transition-colors"
aria-label={`${t(nextLabelKey ?? 'nextPage', language)}: ${nextNav.item.metadata.title}`}
>
<div>
<div className="text-xs text-base-content/50">
{t('nextPage', language)}
{t(nextLabelKey ?? 'nextPage', language)}
</div>
<div className="font-medium">{nextPage.metadata.title}</div>
<div className="font-medium">{nextNav.item.metadata.title}</div>
</div>
<ChevronRight className="w-4 h-4 ml-2" />
</Link>
)}
) : null}
</nav>
)}
</div>
Expand Down
74 changes: 64 additions & 10 deletions apps/wiki/app/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const seoConfig = getSEOConfig();
const languageConfigs = getLanguageConfigs();

const urls: MetadataRoute.Sitemap = [];
const entries: Array<{ url: string; realPath?: string | null }> = [];

// 添加首页 (该页会跳转,所以不添加)
// urls.push({
Expand All @@ -25,7 +25,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {

// 为每种语言添加语言首页
for (const langConfig of languageConfigs) {
urls.push({
entries.push({
url: `${seoConfig.siteUrl}${langConfig.code}`,
});

Expand All @@ -37,21 +37,20 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
subfolder,
);

const { root: navigationItemRoot, map: navigationItemMap } =
await getDocsNavigationMap(langConfig.code, subfolder);
const { map: navigationItemMap } = await getDocsNavigationMap(
langConfig.code,
subfolder,
);

for (const param of params) {
if (param.slug && param.slug.length > 0) {
const path = param.slug.join('/');
const url = `${seoConfig.siteUrl}${param.language}/${path}`;
const docItem = getDocItemByNavigationMap(navigationItemMap, path);

urls.push({
entries.push({
url,
lastModified:
(docItem &&
(await getFileLastModifiedTime(docItem.realPath))) ||
undefined,
realPath: docItem?.realPath || null,
});
}
}
Expand All @@ -64,5 +63,60 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
}
}

return urls;
const parsedConcurrency = Number(process.env.SITEMAP_LASTMOD_CONCURRENCY);
const concurrency = Math.max(
1,
Number.isFinite(parsedConcurrency) && parsedConcurrency > 0
? Math.floor(parsedConcurrency)
: 8,
);

return mapWithConcurrency(entries, concurrency, async (entry) => {
if (!entry.realPath) {
return { url: entry.url };
}

const lastModified = await getFileLastModifiedTime(entry.realPath);

if (!lastModified) {
return { url: entry.url };
}

return { url: entry.url, lastModified };
});
}

async function mapWithConcurrency<T, R>(
items: T[],
limit: number,
mapper: (item: T, index: number) => Promise<R>,
): Promise<R[]> {
const results: R[] = new Array(items.length);
const effectiveLimit =
Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 1;
const workerCount = Math.min(effectiveLimit, items.length);
let currentIndex = 0;

async function worker(): Promise<void> {
while (true) {
const index = currentIndex;
currentIndex += 1;

if (index >= items.length) {
break;
}

results[index] = await mapper(items[index], index);
}
}

if (workerCount === 0) {
return results;
}

const workers = Array.from({ length: workerCount }, () => worker());

await Promise.all(workers);

return results;
}
28 changes: 28 additions & 0 deletions apps/wiki/lib/i18n/translations/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,41 @@
"es": "Página anterior",
"en": "Previous Page"
},
"previousPageSibling": {
"zh-cn": "上一页(同级)",
"zh-hant": "上一頁(同級)",
"ja": "前のページ(同階層)",
"es": "Página anterior (mismo nivel)",
"en": "Previous (Same Level)"
},
"previousPageSequence": {
"zh-cn": "上一页(顺序)",
"zh-hant": "上一頁(順序)",
"ja": "前のページ(連続)",
"es": "Página anterior (secuencia)",
"en": "Previous (Sequential)"
},
"nextPage": {
"zh-cn": "下一页",
"zh-hant": "下一頁",
"ja": "次のページ",
"es": "Página siguiente",
"en": "Next Page"
},
"nextPageSibling": {
"zh-cn": "下一页(同级)",
"zh-hant": "下一頁(同級)",
"ja": "次のページ(同階層)",
"es": "Página siguiente (mismo nivel)",
"en": "Next (Same Level)"
},
"nextPageSequence": {
"zh-cn": "下一页(顺序)",
"zh-hant": "下一頁(順序)",
"ja": "次のページ(連続)",
"es": "Página siguiente (secuencia)",
"en": "Next (Sequential)"
},
"childPages": {
"zh-cn": "子页面",
"zh-hant": "子頁面",
Expand Down
7 changes: 7 additions & 0 deletions apps/wiki/service/directory-service-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,10 @@ export interface DocItemRedirectItem {
displayPath: string;
redirectTo: string;
}

export interface DocNavigationOrderEntry {
previous: string | null;
next: string | null;
}

export type DocNavigationOrderMap = Map<string, DocNavigationOrderEntry>;
67 changes: 61 additions & 6 deletions apps/wiki/service/directory-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import type {
DocItemForClient,
DocItemRedirectItem,
DocMetadata,
DocNavigationOrderEntry,
DocNavigationOrderMap,
} from './directory-service-client';
import { getLocalImagePath } from './path-utils';

Expand Down Expand Up @@ -269,6 +271,50 @@ export async function getDocsNavigationRoot(
): Promise<DocItem> {
return await getDocsNavigationRootInner(language, subfolder);
}
function shouldIncludeInNavigationOrder(item: DocItem): boolean {
if (!item.displayPath) {
return false;
}
if (item.metadata.redirectToSingleChild) {
return false;
}
if (item.slug) {
return true;
}
return Boolean(item.isIndex);
}

function collectNavigationOrder(item: DocItem, result: string[]): void {
if (shouldIncludeInNavigationOrder(item)) {
result.push(item.displayPath);
}
if (item.children) {
for (const child of item.children) {
collectNavigationOrder(child, result);
}
}
}

function buildNavigationOrder(root: DocItem): {
order: string[];
map: DocNavigationOrderMap;
} {
const displayPaths: string[] = [];
collectNavigationOrder(root, displayPaths);
const orderMap: DocNavigationOrderMap = new Map<
string,
DocNavigationOrderEntry
>();
for (let index = 0; index < displayPaths.length; index++) {
const current = displayPaths[index];
const previous = index > 0 ? displayPaths[index - 1] : null;
const next =
index < displayPaths.length - 1 ? displayPaths[index + 1] : null;
orderMap.set(current, { previous, next });
}
return { order: displayPaths, map: orderMap };
}

const getDocsNavigationRootWithMapInner = cache(
async (
language: string,
Expand All @@ -277,6 +323,8 @@ const getDocsNavigationRootWithMapInner = cache(
root: DocItem;
map: Map<string, DocItem>;
redirectMap: Map<string, DocItemRedirectItem>;
order: string[];
orderMap: DocNavigationOrderMap;
}> => {
function addRedirectItem(
item: DocItem,
Expand Down Expand Up @@ -344,7 +392,14 @@ const getDocsNavigationRootWithMapInner = cache(
}
}
collectPaths(rootItem);
return { root: rootItem, map: map, redirectMap: redirectMap };
const { order, map: navigationOrderMap } = buildNavigationOrder(rootItem);
return {
root: rootItem,
map: map,
redirectMap: redirectMap,
order,
orderMap: navigationOrderMap,
};
},
);

Expand All @@ -355,12 +410,12 @@ export async function getDocsNavigationMap(
root: DocItem;
map: Map<string, DocItem>;
redirectMap: Map<string, DocItemRedirectItem>;
order: string[];
orderMap: DocNavigationOrderMap;
}> {
const { root, map, redirectMap } = await getDocsNavigationRootWithMapInner(
language,
subfolder,
);
return { root, map, redirectMap };
const { root, map, redirectMap, order, orderMap } =
await getDocsNavigationRootWithMapInner(language, subfolder);
return { root, map, redirectMap, order, orderMap };
}

const getDocsNavigationForClientInner = cache(
Expand Down