Skip to content

Commit b8f4f36

Browse files
committed
fix: sanitize docs navigation and add section redirects
Prevent prerender from following dead docs links by stripping non-page navigation paths, filtering surround links, and redirecting legacy section URLs to valid docs pages. Made-with: Cursor
1 parent dfcdc91 commit b8f4f36

File tree

8 files changed

+89
-11
lines changed

8 files changed

+89
-11
lines changed

app/app.vue

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
<script setup lang="ts">
2+
import { stripFolderOnlyNavPaths } from '~/utils/sanitize-docs-navigation';
3+
24
const { seo } = useAppConfig();
35
const site = useSiteConfig();
46
57
const { data: navigation } = await useAsyncData('navigation', () => queryCollectionNavigation('docs'));
8+
const { data: docsPathRows } = await useAsyncData('docs-path-index', () => queryCollection('docs').select('path').all());
9+
10+
const docsPathSet = computed(
11+
() => new Set((docsPathRows.value ?? []).map((row) => row.path).filter(Boolean) as string[]),
12+
);
13+
14+
const navigationDisplay = computed(() => stripFolderOnlyNavPaths(navigation.value ?? [], docsPathSet.value));
615
const { data: files } = useLazyAsyncData('search', () => queryCollectionSearchSections('docs'), {
716
server: false,
817
});
@@ -128,6 +137,7 @@ useHead({
128137
});
129138
130139
provide('navigation', navigation);
140+
provide('navigationDisplay', navigationDisplay);
131141
</script>
132142

133143
<template>
@@ -143,7 +153,7 @@ provide('navigation', navigation);
143153
<AppFooter />
144154

145155
<ClientOnly>
146-
<LazyUContentSearch :files="files" :navigation="navigation" />
156+
<LazyUContentSearch :files="files" :navigation="navigationDisplay" />
147157
</ClientOnly>
148158
</UApp>
149159
</template>

app/components/AppHeader.vue

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,20 @@
3333
</template>
3434

3535
<template #body>
36-
<UContentNavigation highlight :navigation="navigation" />
36+
<UContentNavigation highlight :navigation="mobileNavigation" />
3737
</template>
3838
</UHeader>
3939
</template>
4040

4141
<script setup lang="ts">
4242
import type { ContentNavigationItem } from '@nuxt/content';
4343
44-
const navigation = inject<Ref<ContentNavigationItem[]>>('navigation');
44+
const navigation = inject<Ref<ContentNavigationItem[]>>('navigation', ref([]));
45+
const navigationDisplay = inject<ComputedRef<ContentNavigationItem[]>>('navigationDisplay');
46+
47+
const mobileNavigation = computed(
48+
() => navigationDisplay?.value ?? navigation.value,
49+
);
4550
4651
const { header } = useAppConfig();
4752
</script>

app/components/AppSurround.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ const props = defineProps<{
55
surround: ContentNavigationItem[];
66
}>();
77
8-
const prev = computed(() => props.surround[0]);
9-
const next = computed(() => props.surround[1]);
8+
const prev = computed(() => (props.surround[0]?.path ? props.surround[0] : null));
9+
const next = computed(() => (props.surround[1]?.path ? props.surround[1] : null));
1010
</script>
1111

1212
<template>

app/error.vue

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script setup lang="ts">
22
import type { NuxtError } from '#app';
3+
import { stripFolderOnlyNavPaths } from '~/utils/sanitize-docs-navigation';
34
45
defineProps<{
56
error: NuxtError;
@@ -17,11 +18,20 @@ useSeoMeta({
1718
});
1819
1920
const { data: navigation } = await useAsyncData('navigation', () => queryCollectionNavigation('docs'));
21+
const { data: docsPathRows } = await useAsyncData('docs-path-index', () => queryCollection('docs').select('path').all());
22+
23+
const docsPathSet = computed(
24+
() => new Set((docsPathRows.value ?? []).map((row) => row.path).filter(Boolean) as string[]),
25+
);
26+
27+
const navigationDisplay = computed(() => stripFolderOnlyNavPaths(navigation.value ?? [], docsPathSet.value));
28+
2029
const { data: files } = useLazyAsyncData('search', () => queryCollectionSearchSections('docs'), {
2130
server: false,
2231
});
2332
2433
provide('navigation', navigation);
34+
provide('navigationDisplay', navigationDisplay);
2535
</script>
2636

2737
<template>
@@ -33,7 +43,7 @@ provide('navigation', navigation);
3343
<AppFooter />
3444

3545
<ClientOnly>
36-
<LazyUContentSearch :files="files" :navigation="navigation" />
46+
<LazyUContentSearch :files="files" :navigation="navigationDisplay" />
3747
</ClientOnly>
3848
</UApp>
3949
</template>

app/layouts/docs.vue

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
<script setup lang="ts">
22
import type { ContentNavigationItem } from '@nuxt/content';
33
4-
const navigation = inject<Ref<ContentNavigationItem[]>>('navigation', ref([]));
5-
const docsNavigation = computed(() => navigation.value.find((item) => item.path === '/docs')?.children || []);
4+
const navigationDisplay = inject<ComputedRef<ContentNavigationItem[]>>(
5+
'navigationDisplay',
6+
computed(() => []),
7+
);
8+
const docsNavigation = computed(
9+
() => navigationDisplay.value.find((item) => item.path === '/docs')?.children || [],
10+
);
611
</script>
712

813
<template>

app/pages/docs/[...slug].vue

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ const navigation = inject<Ref<ContentNavigationItem[]>>('navigation', ref([]));
1717
1818
const route = useRoute();
1919
20+
const { data: docsPathRows } = await useAsyncData('docs-path-index', () => queryCollection('docs').select('path').all());
21+
const docsPathSet = computed(
22+
() => new Set((docsPathRows.value ?? []).map((row) => row.path).filter(Boolean) as string[]),
23+
);
24+
2025
const { data: page } = await useAsyncData(kebabCase(route.path), () => queryCollection('docs').path(route.path).first());
2126
if (!page.value) {
2227
throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true });
@@ -28,10 +33,14 @@ const { data: surround } = await useAsyncData(`${kebabCase(route.path)}-surround
2833
});
2934
});
3035
36+
const surroundDisplay = computed(() =>
37+
(surround.value ?? []).filter((item) => item?.path && docsPathSet.value.has(item.path)),
38+
);
39+
3140
const breadcrumb: ComputedRef<BreadcrumbLink[]> = computed(() =>
3241
mapContentNavigation(findPageBreadcrumb(navigation.value, page.value?.path)).map((link) => ({
3342
label: link.label,
34-
to: link.to,
43+
to: link.to && docsPathSet.value.has(link.to) ? link.to : undefined,
3544
})),
3645
);
3746
@@ -97,7 +106,8 @@ const communityLinks = computed(() => [
97106
{
98107
icon: 'i-heroicons-lifebuoy-solid',
99108
label: 'Contributing',
100-
to: '/docs/contributing',
109+
to: 'https://github.com/vercube/vercube/blob/main/CONTRIBUTING.md',
110+
target: '_blank',
101111
},
102112
]);
103113
</script>
@@ -134,7 +144,7 @@ const communityLinks = computed(() => [
134144
</UButton>
135145
</div>
136146
</AppDivider>
137-
<AppSurround :surround="surround as any" />
147+
<AppSurround :surround="surroundDisplay as any" />
138148
</div>
139149
</UPageBody>
140150

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type { ContentNavigationItem } from '@nuxt/content';
2+
3+
/**
4+
* Drop `path` on nav nodes that don't map to a real markdown page.
5+
* Keeps the section tree intact while preventing crawlers/users from following dead `/docs/...` links.
6+
*/
7+
export function stripFolderOnlyNavPaths(
8+
items: ContentNavigationItem[],
9+
pathsWithPage: Set<string>,
10+
): ContentNavigationItem[] {
11+
return items.map((item) => {
12+
const children = item.children?.length
13+
? stripFolderOnlyNavPaths(item.children, pathsWithPage)
14+
: item.children;
15+
16+
const path = item.path;
17+
const pathIsPage = path ? pathsWithPage.has(path) : false;
18+
19+
if (path && !pathIsPage) {
20+
const { path: _drop, ...rest } = item;
21+
return { ...rest, children } as ContentNavigationItem;
22+
}
23+
24+
return { ...item, children } as ContentNavigationItem;
25+
});
26+
}

nuxt.config.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,18 @@ export default defineNuxtConfig({
5050
routeRules: {
5151
'/': { prerender: true },
5252
'/docs': { redirect: { statusCode: 301, to: '/docs/getting-started' } },
53+
'/docs/contributing': {
54+
redirect: { statusCode: 301, to: 'https://github.com/vercube/vercube/blob/main/CONTRIBUTING.md' },
55+
},
56+
'/docs/core-features': { redirect: { statusCode: 301, to: '/docs/core-features/configuration' } },
57+
'/docs/modules': { redirect: { statusCode: 301, to: '/docs/modules/mcp' } },
58+
'/docs/advanced': { redirect: { statusCode: 301, to: '/docs/advanced/custom-decorator' } },
59+
'/docs/modules/web-sockets': { redirect: { statusCode: 301, to: '/docs/modules/web-sockets/overview' } },
60+
'/docs/modules/auth': { redirect: { statusCode: 301, to: '/docs/modules/auth/overview' } },
61+
'/docs/modules/storage': { redirect: { statusCode: 301, to: '/docs/modules/storage/overview' } },
62+
'/docs/modules/logger': { redirect: { statusCode: 301, to: '/docs/modules/logger/overview' } },
63+
'/docs/modules/serverless': { redirect: { statusCode: 301, to: '/docs/modules/serverless/overview' } },
64+
'/docs/core/validation': { redirect: { statusCode: 301, to: '/docs/core-features/validation' } },
5365
// Static hosting (GitHub Pages): docs must be prerendered so crawlers and OG validators get real
5466
// HTML with meta tags. Client-only shells look “empty” to bots.
5567
'/docs/**': { prerender: true },

0 commit comments

Comments
 (0)