Date: Thu, 2 Jul 2026 16:33:51 +0200
Subject: [PATCH 4/5] refactor(docs): make section redirect fully dynamic with
fail-safe fallback
- Detect section roots from the navigation tree instead of a hard-coded path
regex, so new top-level (or nested) sections redirect automatically
- Add a fallback map for the header-linked roots so they never 404 if the
navigation tree can't be loaded (e.g. transient content failure)
- Drop the redundant redirect comment from nuxt.config.ts (the middleware is
self-documenting)
---
.../app/middleware/section-redirect.global.ts | 54 +++++++++++--------
apps/docs/nuxt.config.ts | 4 --
2 files changed, 33 insertions(+), 25 deletions(-)
diff --git a/apps/docs/app/middleware/section-redirect.global.ts b/apps/docs/app/middleware/section-redirect.global.ts
index e1033f6c4..1e8b0db55 100644
--- a/apps/docs/app/middleware/section-redirect.global.ts
+++ b/apps/docs/app/middleware/section-redirect.global.ts
@@ -1,24 +1,21 @@
import type { ContentNavigationItem } from "@nuxt/content";
// Section roots (e.g. /components, /utilities, /documentation/design) have no
-// index page of their own. Rather than hard-coding a redirect target per
-// section — which silently points at the wrong page whenever the sidebar is
-// reordered or a higher-sorted page is added — resolve the first entry from the
-// live navigation tree and redirect there. The redirect therefore always tracks
-// whatever the sidebar shows first.
+// index page. Redirect each to its first sidebar entry, resolved from the live
+// navigation tree — so the target never goes stale when the sidebar is reordered,
+// and brand-new sections (top-level or nested) work without touching this file.
export default defineNuxtRouteMiddleware(async (to) => {
- // Only the docs sections use this pattern; skip the landing page and the rest.
- if (!/^\/(documentation|components|utilities)(\/|$)/.test(to.path)) return;
+ // The landing page is the only non-content top-level route; skip it so it does
+ // not wait on the navigation query. Every other route is cheap to check.
+ if (to.path === "/") return;
const path = to.path.replace(/\/+$/, "") || "/";
- // Own cache key (not Docus' "navigation_docs") so we don't clash with its
- // useAsyncData options; the query result is cached per request either way.
- // Mirror Docus' transform: strip a `/docs` wrapper level if one exists so the
- // tree (and its order) matches the sidebar exactly.
const { data: navigation } = await useAsyncData(
"section-redirect:navigation",
() => queryCollectionNavigation("docs"),
+ // Mirror Docus' transform: strip a `/docs` wrapper level if one exists so the
+ // tree (and its order) matches the sidebar exactly.
{
transform: (data: ContentNavigationItem[]) =>
data.find((item) => item.path === "/docs")?.children ?? data,
@@ -26,20 +23,35 @@ export default defineNuxtRouteMiddleware(async (to) => {
);
const nav = navigation.value;
- if (!nav) return;
- const node = findNode(nav, path);
- // Leaf pages have no children; only section nodes are redirected.
- if (!node?.children?.length) return;
-
- const target = firstLeafPath(node);
- if (target && target !== path) {
- // 302 (temporary): the target is derived from navigation order and can
- // change when pages are added or reordered, so it must not be hard-cached.
- return navigateTo(target, { redirectCode: 302, replace: true });
+ if (nav) {
+ const node = findNode(nav, path);
+ // Only section nodes (those with children) redirect; leaf pages and unknown
+ // routes fall through to normal rendering / 404.
+ if (!node?.children?.length) return;
+ const target = firstLeafPath(node);
+ if (target && target !== path) {
+ // 302 (temporary): the target is derived from navigation order and can
+ // change as pages are added or reordered, so it must not be hard-cached.
+ return navigateTo(target, { redirectCode: 302, replace: true });
+ }
+ return;
}
+
+ // Fail-safe: if the navigation tree can't be loaded, the section entry points
+ // linked from the header (see useMainNav) must still resolve instead of 404ing.
+ const fallback = SECTION_ROOT_FALLBACKS[path];
+ if (fallback) return navigateTo(fallback, { redirectCode: 302, replace: true });
});
+// Only the header-linked roots need a hard-coded safety net; deeper section roots
+// are non-critical if navigation is momentarily unavailable.
+const SECTION_ROOT_FALLBACKS: Record = {
+ "/documentation": "/documentation/getting-started/installation",
+ "/components": "/components/action-menu",
+ "/utilities": "/utilities/components/inset",
+};
+
function findNode(
items: ContentNavigationItem[],
path: string,
diff --git a/apps/docs/nuxt.config.ts b/apps/docs/nuxt.config.ts
index ac9a1fc3c..6e2980e3f 100644
--- a/apps/docs/nuxt.config.ts
+++ b/apps/docs/nuxt.config.ts
@@ -80,10 +80,6 @@ export default defineNuxtConfig({
// per environment with NUXT_SITE_URL.
url: "https://meteor.shopware.com",
},
- // Section roots (/components, /utilities, /documentation/*) have no index page.
- // Instead of hard-coding a redirect target per section (which goes stale when
- // the sidebar is reordered), app/middleware/section-redirect.global.ts resolves
- // the first entry from the live navigation tree and redirects there.
// modules/ is auto-scanned, so meteor-components and component-examples load
// automatically. meteor-components registers its work via the
// `component-meta:extend` hook (fired during the build), so module order
From 469a895f5d8e4a6649561fdd02fb8d4bd89a2c56 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fabian=20H=C3=BCske?=
<33117553+fabianhueske@users.noreply.github.com>
Date: Thu, 2 Jul 2026 16:48:29 +0200
Subject: [PATCH 5/5] refactor(docs): dedupe nav payload in redirect
middleware, DRY header links
- Read the navigation tree Docus already fetched via useNuxtData instead of
fetching a second copy under a separate key (avoids serialising the whole
nav tree into the payload twice on every docs page); query directly only as
an SSR fallback
- Extract shared componentSlug/componentSourcePaths computeds so the GitHub and
Storybook link builders no longer duplicate the route guard and source lookup
---
.../app/components/DocsPageHeaderLinks.vue | 40 ++++++++++---------
.../app/middleware/section-redirect.global.ts | 23 ++++++-----
2 files changed, 34 insertions(+), 29 deletions(-)
diff --git a/apps/docs/app/components/DocsPageHeaderLinks.vue b/apps/docs/app/components/DocsPageHeaderLinks.vue
index d1fd74ef5..a22d96591 100644
--- a/apps/docs/app/components/DocsPageHeaderLinks.vue
+++ b/apps/docs/app/components/DocsPageHeaderLinks.vue
@@ -22,16 +22,26 @@ const mcpServerUrl = computed(
() => `${window?.location?.origin}${joinURL(appBaseURL, mcpRoute)}`,
);
-// Link a component page to its source folder on GitHub. The slug -> folder map
-// is provided at build time by modules/meteor-components.ts.
-const componentSourceUrl = computed(() => {
+// The slug -> repo-relative source folder map, provided at build time by
+// modules/meteor-components.ts. Its keys double as the set of known components.
+const componentSourcePaths = computed(
+ () =>
+ (runtimeConfig.public.componentSourcePaths ?? {}) as Record,
+);
+
+// The current component slug, or undefined outside a component page. Both the
+// GitHub and Storybook links are gated on this so they only show for real
+// components and always appear together.
+const componentSlug = computed(() => {
if (!route.path.startsWith("/components/")) return undefined;
- const slug = route.path.split("/").filter(Boolean).pop() ?? "";
- const sources = (runtimeConfig.public.componentSourcePaths ?? {}) as Record<
- string,
- string
- >;
- const path = sources[slug];
+ const slug = route.path.split("/").filter(Boolean).pop();
+ return slug && componentSourcePaths.value[slug] ? slug : undefined;
+});
+
+// Link a component page to its source folder on GitHub.
+const componentSourceUrl = computed(() => {
+ if (!componentSlug.value) return undefined;
+ const path = componentSourcePaths.value[componentSlug.value];
const github = appConfig.github as
| { url?: string; branch?: string }
| undefined;
@@ -42,17 +52,11 @@ const componentSourceUrl = computed(() => {
// Link a component page to its Storybook entry. The docs slug matches the
// Storybook id prefix (e.g. "data-table" -> components-data-table); Storybook
// resolves that to the component's autodocs (or first story) automatically.
-// Gated on the same source map as the GitHub link so the two buttons pair up.
const componentStorybookUrl = computed(() => {
- if (!route.path.startsWith("/components/")) return undefined;
- const slug = route.path.split("/").filter(Boolean).pop() ?? "";
- const sources = (runtimeConfig.public.componentSourcePaths ?? {}) as Record<
- string,
- string
- >;
+ if (!componentSlug.value) return undefined;
const storybook = appConfig.storybook as { url?: string } | undefined;
- if (!sources[slug] || !storybook?.url) return undefined;
- return `${storybook.url}/?path=/docs/components-${slug}`;
+ if (!storybook?.url) return undefined;
+ return `${storybook.url}/?path=/docs/components-${componentSlug.value}`;
});
const items = computed(() => [
diff --git a/apps/docs/app/middleware/section-redirect.global.ts b/apps/docs/app/middleware/section-redirect.global.ts
index 1e8b0db55..92cfcd740 100644
--- a/apps/docs/app/middleware/section-redirect.global.ts
+++ b/apps/docs/app/middleware/section-redirect.global.ts
@@ -11,18 +11,18 @@ export default defineNuxtRouteMiddleware(async (to) => {
const path = to.path.replace(/\/+$/, "") || "/";
- const { data: navigation } = await useAsyncData(
- "section-redirect:navigation",
- () => queryCollectionNavigation("docs"),
+ // Reuse the navigation tree Docus already fetched (keyed "navigation_docs")
+ // instead of fetching our own copy, which would be serialised into the payload
+ // a second time on every docs page. On a direct SSR hit the middleware can run
+ // before Docus' fetch, so query the collection directly as a fallback.
+ const cached = useNuxtData("navigation_docs");
+ let nav = cached.data.value ?? undefined;
+ if (!nav) {
+ const data = await queryCollectionNavigation("docs").catch(() => undefined);
// Mirror Docus' transform: strip a `/docs` wrapper level if one exists so the
// tree (and its order) matches the sidebar exactly.
- {
- transform: (data: ContentNavigationItem[]) =>
- data.find((item) => item.path === "/docs")?.children ?? data,
- },
- );
-
- const nav = navigation.value;
+ nav = data?.find((item) => item.path === "/docs")?.children ?? data;
+ }
if (nav) {
const node = findNode(nav, path);
@@ -41,7 +41,8 @@ export default defineNuxtRouteMiddleware(async (to) => {
// Fail-safe: if the navigation tree can't be loaded, the section entry points
// linked from the header (see useMainNav) must still resolve instead of 404ing.
const fallback = SECTION_ROOT_FALLBACKS[path];
- if (fallback) return navigateTo(fallback, { redirectCode: 302, replace: true });
+ if (fallback)
+ return navigateTo(fallback, { redirectCode: 302, replace: true });
});
// Only the header-linked roots need a hard-coded safety net; deeper section roots