@djangocfg/layouts 2.1.276 → 2.1.279

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/layouts",
3
- "version": "2.1.276",
3
+ "version": "2.1.279",
4
4
  "description": "Simple, straightforward layout components for Next.js - import and use with props",
5
5
  "keywords": [
6
6
  "layouts",
@@ -74,19 +74,19 @@
74
74
  "check": "tsc --noEmit"
75
75
  },
76
76
  "peerDependencies": {
77
- "@djangocfg/api": "^2.1.276",
78
- "@djangocfg/centrifugo": "^2.1.276",
79
- "@djangocfg/debuger": "^2.1.276",
80
- "@djangocfg/i18n": "^2.1.276",
81
- "@djangocfg/monitor": "^2.1.276",
82
- "@djangocfg/ui-core": "^2.1.276",
83
- "@djangocfg/ui-nextjs": "^2.1.276",
84
- "@djangocfg/ui-tools": "^2.1.276",
77
+ "@djangocfg/api": "^2.1.279",
78
+ "@djangocfg/centrifugo": "^2.1.279",
79
+ "@djangocfg/debuger": "^2.1.279",
80
+ "@djangocfg/i18n": "^2.1.279",
81
+ "@djangocfg/monitor": "^2.1.279",
82
+ "@djangocfg/ui-core": "^2.1.279",
83
+ "@djangocfg/ui-nextjs": "^2.1.279",
84
+ "@djangocfg/ui-tools": "^2.1.279",
85
85
  "@hookform/resolvers": "^5.2.2",
86
86
  "consola": "^3.4.2",
87
87
  "lucide-react": "^0.545.0",
88
88
  "moment": "^2.30.1",
89
- "next": "^16.0.10",
89
+ "next": "^16.2.2",
90
90
  "p-retry": "^7.0.0",
91
91
  "react": "^19.1.0",
92
92
  "react-dom": "^19.1.0",
@@ -110,15 +110,15 @@
110
110
  "uuid": "^11.1.0"
111
111
  },
112
112
  "devDependencies": {
113
- "@djangocfg/api": "^2.1.276",
114
- "@djangocfg/centrifugo": "^2.1.276",
115
- "@djangocfg/debuger": "^2.1.276",
116
- "@djangocfg/i18n": "^2.1.276",
117
- "@djangocfg/monitor": "^2.1.276",
118
- "@djangocfg/typescript-config": "^2.1.276",
119
- "@djangocfg/ui-core": "^2.1.276",
120
- "@djangocfg/ui-nextjs": "^2.1.276",
121
- "@djangocfg/ui-tools": "^2.1.276",
113
+ "@djangocfg/api": "^2.1.279",
114
+ "@djangocfg/centrifugo": "^2.1.279",
115
+ "@djangocfg/debuger": "^2.1.279",
116
+ "@djangocfg/i18n": "^2.1.279",
117
+ "@djangocfg/monitor": "^2.1.279",
118
+ "@djangocfg/typescript-config": "^2.1.279",
119
+ "@djangocfg/ui-core": "^2.1.279",
120
+ "@djangocfg/ui-nextjs": "^2.1.279",
121
+ "@djangocfg/ui-tools": "^2.1.279",
122
122
  "@types/node": "^24.7.2",
123
123
  "@types/react": "^19.1.0",
124
124
  "@types/react-dom": "^19.1.0",
@@ -6,7 +6,6 @@
6
6
 
7
7
  'use client';
8
8
 
9
- import Link from 'next/link';
10
9
  import React, { useEffect, useState } from 'react';
11
10
  import { Laptop, Moon, Sun } from 'lucide-react';
12
11
 
@@ -14,6 +13,7 @@ import { Button } from '@djangocfg/ui-core/components';
14
13
  import { useThemeContext } from '@djangocfg/ui-nextjs/theme';
15
14
 
16
15
  import { LocaleSwitcher } from '../../../_components/LocaleSwitcher';
16
+ import { SmartNavLink } from '../../primitives/SmartNavLink';
17
17
  import { FooterMenuSections } from './FooterMenuSections';
18
18
  import { FooterProjectInfo } from './FooterProjectInfo';
19
19
 
@@ -132,13 +132,13 @@ export function DefaultFooter({ config }: DefaultFooterProps) {
132
132
  {link.label}
133
133
  </a>
134
134
  ) : (
135
- <Link
135
+ <SmartNavLink
136
136
  key={link.path}
137
137
  href={link.path}
138
138
  className="text-sm text-muted-foreground hover:text-foreground transition-colors"
139
139
  >
140
140
  {link.label}
141
- </Link>
141
+ </SmartNavLink>
142
142
  )
143
143
  )}
144
144
  </div>
@@ -195,13 +195,13 @@ export function DefaultFooter({ config }: DefaultFooterProps) {
195
195
  {link.label}
196
196
  </a>
197
197
  ) : (
198
- <Link
198
+ <SmartNavLink
199
199
  key={link.path}
200
200
  href={link.path}
201
201
  className="text-xs text-muted-foreground hover:text-primary transition-colors"
202
202
  >
203
203
  {link.label}
204
- </Link>
204
+ </SmartNavLink>
205
205
  )
206
206
  )}
207
207
  </div>
@@ -302,9 +302,9 @@ export function DefaultFooter({ config }: DefaultFooterProps) {
302
302
  {link.label}
303
303
  </a>
304
304
  ) : (
305
- <Link key={link.path} href={link.path} className="hover:text-foreground transition-colors whitespace-nowrap">
305
+ <SmartNavLink key={link.path} href={link.path} className="hover:text-foreground transition-colors whitespace-nowrap">
306
306
  {link.label}
307
- </Link>
307
+ </SmartNavLink>
308
308
  )
309
309
  )}
310
310
  </div>
@@ -1,8 +1,8 @@
1
1
  'use client';
2
2
 
3
- import Link from 'next/link';
4
3
  import React from 'react';
5
4
 
5
+ import { SmartNavLink } from '../../primitives/SmartNavLink';
6
6
  import { DjangoCFGLogo } from './DjangoCFGLogo';
7
7
 
8
8
  import type { FooterLink } from './types';
@@ -95,13 +95,13 @@ export function FooterBottom({
95
95
  {link.label}
96
96
  </a>
97
97
  ) : (
98
- <Link
98
+ <SmartNavLink
99
99
  key={link.path}
100
100
  href={link.path}
101
101
  className="text-xs text-muted-foreground hover:text-primary transition-colors"
102
102
  >
103
103
  {link.label}
104
- </Link>
104
+ </SmartNavLink>
105
105
  )
106
106
  )}
107
107
  </div>
@@ -1,8 +1,9 @@
1
1
  'use client';
2
2
 
3
- import Link from 'next/link';
4
3
  import React from 'react';
5
4
 
5
+ import { SmartNavLink } from '../../primitives/SmartNavLink';
6
+
6
7
  import type { FooterMenuSection } from './types';
7
8
 
8
9
  export interface FooterMenuSectionsProps {
@@ -33,12 +34,12 @@ export function FooterMenuSections({
33
34
  <ul className="space-y-2">
34
35
  {section.items.map((item) => (
35
36
  <li key={item.path}>
36
- <Link
37
+ <SmartNavLink
37
38
  href={item.path}
38
39
  className="text-sm text-foreground/90 hover:text-foreground transition-colors"
39
40
  >
40
41
  {item.label}
41
- </Link>
42
+ </SmartNavLink>
42
43
  </li>
43
44
  ))}
44
45
  </ul>
@@ -29,8 +29,26 @@ export type { UseNavbarScrollOptions, UseNavbarScrollReturn } from './hooks/useN
29
29
  export type { UseDropdownMenuReturn } from './hooks/useDropdownMenu';
30
30
 
31
31
  // Primitives (for users who want to build a custom navbar)
32
- export { NavBrand, NavActions, NavActionItem, NavDesktopItems, ThemeBrandMark, ThemeBrandMarkImg } from './primitives';
33
- export type { NavAction, ThemeBrandMarkProps, ThemeBrandMarkImgProps } from './primitives';
32
+ export {
33
+ NavBrand,
34
+ NavActions,
35
+ NavActionItem,
36
+ NavDesktopItems,
37
+ ThemeBrandMark,
38
+ ThemeBrandMarkImg,
39
+ ExternalPrefixesProvider,
40
+ useExternalPrefixes,
41
+ isExternalPrefixHref,
42
+ SmartNavLink,
43
+ } from './primitives';
44
+ export type {
45
+ NavAction,
46
+ ThemeBrandMarkProps,
47
+ ThemeBrandMarkImgProps,
48
+ ExternalPrefixes,
49
+ ExternalPrefixesProviderProps,
50
+ SmartNavLinkProps,
51
+ } from './primitives';
34
52
 
35
53
  // Navbar variants
36
54
  export * from './navbars';
@@ -11,6 +11,7 @@ import React from 'react';
11
11
  import { cn } from '@djangocfg/ui-core/lib';
12
12
 
13
13
  import { publicFloatingChromeClassName } from '../../publicShellShadow';
14
+ import { ExternalPrefixesProvider } from '../../primitives/ExternalPrefixesContext';
14
15
  import type { NavAction } from '../../primitives/NavActionItem';
15
16
  import { NavbarShell } from '../../shared';
16
17
  import type {
@@ -51,6 +52,14 @@ export interface FloatingNavbarConfig {
51
52
  actionsLeadingSlot?: React.ReactNode;
52
53
  /** Arbitrary ReactNode after the mobile toggle. */
53
54
  actionsTrailingSlot?: React.ReactNode;
55
+ /**
56
+ * Path prefixes that should be navigated with a plain `<a>` (full page load)
57
+ * instead of `next/link`. Use this for routes served by a catch-all handler
58
+ * outside the Next.js App Router (e.g. Nextra at `/docs/*`) — those routes
59
+ * cannot return an RSC payload, so `next/link` client navigation fails.
60
+ * @default []
61
+ */
62
+ externalPrefixes?: readonly string[];
54
63
  }
55
64
 
56
65
  export interface FloatingNavbarProps {
@@ -62,6 +71,7 @@ export function FloatingNavbar({ config }: FloatingNavbarProps) {
62
71
  const rounding = config.shell?.rounding;
63
72
  const containerClassName = config.shell?.className;
64
73
  const position = config.navbarPosition ?? 'sticky';
74
+ const externalPrefixes = config.externalPrefixes;
65
75
 
66
76
  const outerClassName = cn(
67
77
  position === 'fixed' ? 'fixed' : position === 'static' ? 'static' : 'sticky',
@@ -77,7 +87,7 @@ export function FloatingNavbar({ config }: FloatingNavbarProps) {
77
87
  );
78
88
 
79
89
  return (
80
- <>
90
+ <ExternalPrefixesProvider value={externalPrefixes}>
81
91
  <NavbarShell
82
92
  variant="floating"
83
93
  position={position}
@@ -112,6 +122,6 @@ export function FloatingNavbar({ config }: FloatingNavbarProps) {
112
122
  containerClassName={containerClassName}
113
123
  rounding={rounding}
114
124
  />
115
- </>
125
+ </ExternalPrefixesProvider>
116
126
  );
117
127
  }
@@ -10,6 +10,7 @@ import React from 'react';
10
10
 
11
11
  import { cn } from '@djangocfg/ui-core/lib';
12
12
 
13
+ import { ExternalPrefixesProvider } from '../../primitives/ExternalPrefixesContext';
13
14
  import type { NavAction } from '../../primitives/NavActionItem';
14
15
  import { NavbarShell } from '../../shared';
15
16
  import type {
@@ -50,6 +51,14 @@ export interface FlushNavbarConfig {
50
51
  actionsLeadingSlot?: React.ReactNode;
51
52
  /** Arbitrary ReactNode after the mobile toggle. */
52
53
  actionsTrailingSlot?: React.ReactNode;
54
+ /**
55
+ * Path prefixes that should be navigated with a plain `<a>` (full page load)
56
+ * instead of `next/link`. Use this for routes served by a catch-all handler
57
+ * outside the Next.js App Router (e.g. Nextra at `/docs/*`) — those routes
58
+ * cannot return an RSC payload, so `next/link` client navigation fails.
59
+ * @default []
60
+ */
61
+ externalPrefixes?: readonly string[];
53
62
  }
54
63
 
55
64
  export interface FlushNavbarProps {
@@ -60,6 +69,7 @@ export function FlushNavbar({ config }: FlushNavbarProps) {
60
69
  const navigation = config.navigation ?? [];
61
70
  const containerClassName = config.shell?.className;
62
71
  const position = config.navbarPosition ?? 'sticky';
72
+ const externalPrefixes = config.externalPrefixes;
63
73
 
64
74
  const outerClassName = cn(
65
75
  position === 'fixed' ? 'fixed' : position === 'static' ? 'static' : 'sticky',
@@ -73,7 +83,7 @@ export function FlushNavbar({ config }: FlushNavbarProps) {
73
83
  );
74
84
 
75
85
  return (
76
- <>
86
+ <ExternalPrefixesProvider value={externalPrefixes}>
77
87
  <NavbarShell
78
88
  variant="flush"
79
89
  position={position}
@@ -107,6 +117,6 @@ export function FlushNavbar({ config }: FlushNavbarProps) {
107
117
  userMenu={config.userMenu}
108
118
  containerClassName={containerClassName}
109
119
  />
110
- </>
120
+ </ExternalPrefixesProvider>
111
121
  );
112
122
  }
@@ -17,6 +17,7 @@ import { Button } from '@djangocfg/ui-core/components';
17
17
  import { cn } from '@djangocfg/ui-core/lib';
18
18
 
19
19
  import { UserMenu } from '../../../_components/UserMenu';
20
+ import { ExternalPrefixesProvider } from '../../primitives/ExternalPrefixesContext';
20
21
  import { NavActionItem, type NavAction } from '../../primitives/NavActionItem';
21
22
  import { NavbarShell, type NavbarActionsContext } from '../../shared';
22
23
  import type {
@@ -67,6 +68,15 @@ export interface MinimalNavbarConfig {
67
68
  * @default 'mx-auto max-w-[1400px] px-4 sm:px-6 lg:px-10'
68
69
  */
69
70
  containerClassName?: string;
71
+
72
+ /**
73
+ * Path prefixes that should be navigated with a plain `<a>` (full page load)
74
+ * instead of `next/link`. Use this for routes served by a catch-all handler
75
+ * outside the Next.js App Router (e.g. Nextra at `/docs/*`) — those routes
76
+ * cannot return an RSC payload, so `next/link` client navigation fails.
77
+ * @default []
78
+ */
79
+ externalPrefixes?: readonly string[];
70
80
  }
71
81
 
72
82
  export interface MinimalNavbarProps {
@@ -121,6 +131,7 @@ export function MinimalNavbar({ config }: MinimalNavbarProps) {
121
131
  const position = config.navbarPosition ?? 'sticky';
122
132
  const transparent = config.transparent ?? true;
123
133
  const containerClassName = config.containerClassName ?? 'mx-auto max-w-[1400px] px-4 sm:px-6 lg:px-10';
134
+ const externalPrefixes = config.externalPrefixes;
124
135
 
125
136
  const outerClassName = cn(
126
137
  position === 'fixed' ? 'fixed' : position === 'static' ? 'static' : 'sticky',
@@ -131,7 +142,7 @@ export function MinimalNavbar({ config }: MinimalNavbarProps) {
131
142
  const shapeClassName = 'w-full rounded-none border-0 shadow-none';
132
143
 
133
144
  return (
134
- <>
145
+ <ExternalPrefixesProvider value={externalPrefixes}>
135
146
  <NavbarShell
136
147
  variant="minimal"
137
148
  position={position}
@@ -164,6 +175,6 @@ export function MinimalNavbar({ config }: MinimalNavbarProps) {
164
175
  userMenu={config.userMenu}
165
176
  containerClassName={containerClassName}
166
177
  />
167
- </>
178
+ </ExternalPrefixesProvider>
168
179
  );
169
180
  }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * ExternalPrefixesContext
3
+ *
4
+ * Provides a list of path prefixes that should be treated as "outside" the
5
+ * Next.js App Router (e.g. a catch-all served by Nextra). Consumers inside the
6
+ * navbar (`SmartNavLink`) check whether the `href` starts with any configured
7
+ * prefix; if so, they render a native `<a>` (full page load) instead of a
8
+ * `next/link` that would fetch an RSC payload the route cannot produce.
9
+ *
10
+ * Default: `[]` — so behaviour is unchanged for consumers who do not opt in.
11
+ */
12
+
13
+ 'use client';
14
+
15
+ import React, { createContext, useContext, useMemo, type ReactNode } from 'react';
16
+
17
+ export type ExternalPrefixes = readonly string[];
18
+
19
+ const ExternalPrefixesContext = createContext<ExternalPrefixes>([]);
20
+
21
+ export interface ExternalPrefixesProviderProps {
22
+ /** List of path prefixes treated as external to the App Router. */
23
+ value?: ExternalPrefixes;
24
+ children: ReactNode;
25
+ }
26
+
27
+ export function ExternalPrefixesProvider({
28
+ value,
29
+ children,
30
+ }: ExternalPrefixesProviderProps) {
31
+ const resolved = useMemo<ExternalPrefixes>(() => value ?? [], [value]);
32
+ return (
33
+ <ExternalPrefixesContext.Provider value={resolved}>
34
+ {children}
35
+ </ExternalPrefixesContext.Provider>
36
+ );
37
+ }
38
+
39
+ export function useExternalPrefixes(): ExternalPrefixes {
40
+ return useContext(ExternalPrefixesContext);
41
+ }
42
+
43
+ /**
44
+ * Returns true if `href` is a string that starts with any of the provided
45
+ * external prefixes. Non-string hrefs (objects, `next/link` URL shape) always
46
+ * return false — those are App Router internal.
47
+ *
48
+ * Matches the prefix exactly or at a path-boundary (`/`, `?`, `#`) so that
49
+ * `/docs` in the prefix list matches `/docs`, `/docs/`, `/docs/foo`, `/docs?x=1`,
50
+ * `/docs#bar` — but NOT `/docsearch` (which should stay an App Router link).
51
+ */
52
+ export function isExternalPrefixHref(
53
+ href: unknown,
54
+ prefixes: ExternalPrefixes,
55
+ ): href is string {
56
+ if (typeof href !== 'string' || prefixes.length === 0) return false;
57
+ for (const prefix of prefixes) {
58
+ if (!prefix) continue;
59
+ if (href === prefix) return true;
60
+ if (
61
+ href.startsWith(`${prefix}/`) ||
62
+ href.startsWith(`${prefix}?`) ||
63
+ href.startsWith(`${prefix}#`)
64
+ ) {
65
+ return true;
66
+ }
67
+ }
68
+ return false;
69
+ }
@@ -7,11 +7,12 @@
7
7
 
8
8
  'use client';
9
9
 
10
- import Link from 'next/link';
11
10
  import React, { type ReactNode } from 'react';
12
11
 
13
12
  import { cn } from '@djangocfg/ui-core/lib';
14
13
 
14
+ import { SmartNavLink } from './SmartNavLink';
15
+
15
16
  export interface NavAction {
16
17
  label: string;
17
18
  href: string;
@@ -87,8 +88,8 @@ export function NavActionItem({ action, className }: NavActionItemProps) {
87
88
  );
88
89
  }
89
90
  return (
90
- <Link href={action.href} className={cls} onClick={action.onClick}>
91
+ <SmartNavLink href={action.href} className={cls} onClick={action.onClick}>
91
92
  {content}
92
- </Link>
93
+ </SmartNavLink>
93
94
  );
94
95
  }
@@ -1,8 +1,9 @@
1
1
  'use client';
2
2
 
3
- import Link from 'next/link';
4
3
  import React, { type ReactNode } from 'react';
5
4
 
5
+ import { SmartNavLink } from './SmartNavLink';
6
+
6
7
  interface NavBrandProps {
7
8
  brand?: ReactNode;
8
9
  brandHref?: string;
@@ -13,12 +14,12 @@ export function NavBrand({ brand, brandHref = '/' }: NavBrandProps) {
13
14
 
14
15
  if (typeof brand === 'string') {
15
16
  return (
16
- <Link
17
+ <SmartNavLink
17
18
  href={brandHref}
18
19
  className="font-bold text-[15px] text-foreground hover:opacity-90 transition-opacity"
19
20
  >
20
21
  {brand}
21
- </Link>
22
+ </SmartNavLink>
22
23
  );
23
24
  }
24
25
 
@@ -1,7 +1,6 @@
1
1
  'use client';
2
2
 
3
3
  import { ChevronDown } from 'lucide-react';
4
- import Link from 'next/link';
5
4
  import React from 'react';
6
5
 
7
6
  import { Button } from '@djangocfg/ui-core/components';
@@ -11,6 +10,7 @@ import type { NavigationItem } from '../../types';
11
10
  import type { UseDropdownMenuReturn } from '../hooks/useDropdownMenu';
12
11
  import { useResponsiveOverflow } from '../hooks/useResponsiveOverflow';
13
12
  import type { PublicDesktopDropdownRenderer } from '../navbarTypes';
13
+ import { SmartNavLink } from './SmartNavLink';
14
14
 
15
15
  interface NavDesktopItemsProps {
16
16
  /** Full ordered item list; overflow is computed responsively. */
@@ -91,9 +91,9 @@ export function NavDesktopItems({
91
91
  <span className="min-w-0 truncate" title={sub.label}>{sub.label}</span>
92
92
  </a>
93
93
  ) : (
94
- <Link href={sub.href} className={subMenuLinkCls(subActive)}>
94
+ <SmartNavLink href={sub.href} className={subMenuLinkCls(subActive)}>
95
95
  <span className="min-w-0 truncate" title={sub.label}>{sub.label}</span>
96
- </Link>
96
+ </SmartNavLink>
97
97
  )}
98
98
  </div>
99
99
  );
@@ -149,13 +149,13 @@ export function NavDesktopItems({
149
149
 
150
150
  const active = isActivePath(item.href);
151
151
  return (
152
- <Link
152
+ <SmartNavLink
153
153
  key={item.href}
154
154
  href={item.href}
155
155
  className={cn(navItemCls, active && navItemActiveCls)}
156
156
  >
157
157
  <span className={labelCls} title={item.label}>{item.label}</span>
158
- </Link>
158
+ </SmartNavLink>
159
159
  );
160
160
  };
161
161
 
@@ -235,9 +235,9 @@ export function NavDesktopItems({
235
235
  const active = isGroupActive(item);
236
236
  return (
237
237
  <div key={`overflow-${item.href}`} className="rounded-full">
238
- <Link href={item.href} className={subMenuLinkCls(active)}>
238
+ <SmartNavLink href={item.href} className={subMenuLinkCls(active)}>
239
239
  <span className="min-w-0 truncate" title={item.label}>{item.label}</span>
240
- </Link>
240
+ </SmartNavLink>
241
241
  </div>
242
242
  );
243
243
  })}
@@ -0,0 +1,81 @@
1
+ /**
2
+ * SmartNavLink
3
+ *
4
+ * Drop-in replacement for `next/link` used inside PublicLayout navbars/footers.
5
+ *
6
+ * If the `href` is a string that starts with one of the prefixes configured
7
+ * via `ExternalPrefixesProvider`, renders a plain `<a>` (full page navigation).
8
+ * This is required for routes owned by a catch-all handler outside App Router
9
+ * (e.g. Nextra `/docs/*`), where `next/link` client navigation asks for an
10
+ * RSC payload the route cannot produce — resulting in a hard error banner.
11
+ *
12
+ * Otherwise falls back to `next/link` so existing behaviour is preserved.
13
+ */
14
+
15
+ 'use client';
16
+
17
+ import Link, { type LinkProps } from 'next/link';
18
+ import React, { forwardRef, type AnchorHTMLAttributes, type ReactNode } from 'react';
19
+
20
+ import {
21
+ isExternalPrefixHref,
22
+ useExternalPrefixes,
23
+ type ExternalPrefixes,
24
+ } from './ExternalPrefixesContext';
25
+
26
+ type AnchorProps = Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'>;
27
+
28
+ export interface SmartNavLinkProps
29
+ extends Omit<LinkProps, 'href' | 'passHref' | 'legacyBehavior'>,
30
+ AnchorProps {
31
+ href: LinkProps['href'];
32
+ children?: ReactNode;
33
+ /** Optional override — falls back to context value. */
34
+ externalPrefixes?: ExternalPrefixes;
35
+ }
36
+
37
+ export const SmartNavLink = forwardRef<HTMLAnchorElement, SmartNavLinkProps>(
38
+ function SmartNavLink(props, ref) {
39
+ const {
40
+ href,
41
+ children,
42
+ externalPrefixes,
43
+ // LinkProps-specific fields we should NOT forward to a plain <a>.
44
+ prefetch,
45
+ replace,
46
+ scroll,
47
+ shallow,
48
+ locale,
49
+ // Rest are anchor-compatible attributes.
50
+ ...anchorProps
51
+ } = props;
52
+
53
+ const contextPrefixes = useExternalPrefixes();
54
+ const prefixes = externalPrefixes ?? contextPrefixes;
55
+
56
+ const shouldUseAnchor = isExternalPrefixHref(href, prefixes);
57
+
58
+ if (shouldUseAnchor) {
59
+ return (
60
+ <a ref={ref} href={href} {...anchorProps}>
61
+ {children}
62
+ </a>
63
+ );
64
+ }
65
+
66
+ return (
67
+ <Link
68
+ ref={ref}
69
+ href={href}
70
+ prefetch={prefetch}
71
+ replace={replace}
72
+ scroll={scroll}
73
+ shallow={shallow}
74
+ locale={locale}
75
+ {...anchorProps}
76
+ >
77
+ {children}
78
+ </Link>
79
+ );
80
+ },
81
+ );
@@ -5,3 +5,14 @@ export type { NavAction } from './NavActionItem';
5
5
  export { NavDesktopItems } from './NavDesktopItems';
6
6
  export { ThemeBrandMark, ThemeBrandMarkImg } from './ThemeBrandMark';
7
7
  export type { ThemeBrandMarkProps, ThemeBrandMarkImgProps } from './ThemeBrandMark';
8
+ export {
9
+ ExternalPrefixesProvider,
10
+ useExternalPrefixes,
11
+ isExternalPrefixHref,
12
+ } from './ExternalPrefixesContext';
13
+ export type {
14
+ ExternalPrefixes,
15
+ ExternalPrefixesProviderProps,
16
+ } from './ExternalPrefixesContext';
17
+ export { SmartNavLink } from './SmartNavLink';
18
+ export type { SmartNavLinkProps } from './SmartNavLink';
@@ -8,7 +8,6 @@
8
8
  'use client';
9
9
 
10
10
  import { ArrowRight } from 'lucide-react';
11
- import Link from 'next/link';
12
11
  import React, { useMemo } from 'react';
13
12
 
14
13
  import { useAuth } from '@djangocfg/api/auth';
@@ -20,6 +19,7 @@ import { usePathnameWithoutLocale } from '../../../hooks';
20
19
  import { UserMenu } from '../../_components/UserMenu';
21
20
  import { usePublicLayoutOptional } from '../context';
22
21
  import { useMobileNavPanel } from '../hooks';
22
+ import { SmartNavLink } from '../primitives/SmartNavLink';
23
23
 
24
24
  import type { NavigationItem, UserMenuConfig } from '../../types';
25
25
 
@@ -133,7 +133,7 @@ export function MobileDrawerShell(props: MobileDrawerShellProps) {
133
133
  const parentOnlySectionOpen = hasChildNav && anyChildActive;
134
134
  return (
135
135
  <div key={item.href}>
136
- <Link
136
+ <SmartNavLink
137
137
  href={item.href}
138
138
  onClick={closeMobileMenu}
139
139
  title={item.label}
@@ -148,13 +148,13 @@ export function MobileDrawerShell(props: MobileDrawerShellProps) {
148
148
  )}
149
149
  >
150
150
  {item.label}
151
- </Link>
151
+ </SmartNavLink>
152
152
  {hasChildNav && (
153
153
  <div className="ml-3 mt-1.5 space-y-1 border-l border-border/40 pl-3">
154
154
  {childItems.map((subItem) => {
155
155
  const subActive = isActivePath(subItem.href);
156
156
  return (
157
- <Link
157
+ <SmartNavLink
158
158
  key={`${item.href}-${subItem.href}`}
159
159
  href={subItem.href}
160
160
  onClick={closeMobileMenu}
@@ -168,7 +168,7 @@ export function MobileDrawerShell(props: MobileDrawerShellProps) {
168
168
  )}
169
169
  >
170
170
  {subItem.label}
171
- </Link>
171
+ </SmartNavLink>
172
172
  );
173
173
  })}
174
174
  </div>
@@ -182,7 +182,7 @@ export function MobileDrawerShell(props: MobileDrawerShellProps) {
182
182
 
183
183
  {showSignInFooter && (
184
184
  <div className="shrink-0 border-t border-border/50 p-4">
185
- <Link
185
+ <SmartNavLink
186
186
  href={userMenu?.authPath || '/auth'}
187
187
  onClick={closeMobileMenu}
188
188
  className="block"
@@ -194,7 +194,7 @@ export function MobileDrawerShell(props: MobileDrawerShellProps) {
194
194
  aria-hidden
195
195
  />
196
196
  </Button>
197
- </Link>
197
+ </SmartNavLink>
198
198
  </div>
199
199
  )}
200
200
  </div>