Skip to content

Commit b669e38

Browse files
committed
Added back tiny NavAPI call to track folder title/sortOrder vals
1 parent 433b3a5 commit b669e38

4 files changed

Lines changed: 210 additions & 26 deletions

File tree

services/docs/getNavSections.ts

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,14 @@ import {
77
type ApiNavItem,
88
type NavSection,
99
} from "@/util/navTransform";
10-
import { buildMenulinksUrl, navPayloadToApiNavTree } from "@/util/menulinksToApiNav";
10+
import {
11+
applyNavFolderOverlayToMenulinksTree,
12+
buildMenulinksUrl,
13+
buildNavApiUrl,
14+
extractNavApiFolderOverlay,
15+
navPayloadToApiNavTree,
16+
resortMenulinksDerivedTree,
17+
} from "@/util/menulinksToApiNav";
1118
import fallbackNavData from "@/components/navigation/fallback.json";
1219

1320
type FetchOptions = {
@@ -61,12 +68,14 @@ export async function getNavSections(options: FetchOptions = {}): Promise<NavSec
6168
const folderPath = options.path ?? Config.NavFolderPath;
6269
const depth = options.depth ?? Config.NavMenuDepth;
6370
const ttlSeconds = options.ttlSeconds ?? 900;
71+
const navOverlayDepth = Config.NavFolderOverlayDepth;
72+
const languageId = options.languageId ?? Config.LanguageId;
6473

6574
const pathsCacheKey = getCacheKey(
66-
`menulinks|${Config.NavSiteId}|${folderPath}|${depth}|allPaths`
75+
`menulinks|${Config.NavSiteId}|${folderPath}|${depth}|nav${navOverlayDepth}|allPaths`
6776
);
6877
const slugCacheKey = getCacheKey(
69-
`menulinks|${Config.NavSiteId}|${folderPath}|${depth}|${options.currentSlug ?? ""}`
78+
`menulinks|${Config.NavSiteId}|${folderPath}|${depth}|nav${navOverlayDepth}|${options.currentSlug ?? ""}`
7079
);
7180

7281
let sectionsAllForPaths: NavSection[] | undefined =
@@ -80,25 +89,49 @@ export async function getNavSections(options: FetchOptions = {}): Promise<NavSec
8089

8190
try {
8291
const menulinksUrl = buildMenulinksUrl(Config.DotCMSHost, Config.NavSiteId, folderPath, depth);
83-
84-
const res = await fetch(menulinksUrl, {
85-
method: "GET",
86-
headers: Config.Headers,
87-
next: { revalidate: ttlSeconds },
88-
});
89-
90-
if (!res.ok) {
92+
const navUrl = buildNavApiUrl(Config.DotCMSHost, folderPath, navOverlayDepth, languageId);
93+
94+
const [mlRes, navRes] = await Promise.all([
95+
fetch(menulinksUrl, {
96+
method: "GET",
97+
headers: Config.Headers,
98+
next: { revalidate: ttlSeconds },
99+
}),
100+
fetch(navUrl, {
101+
method: "GET",
102+
headers: Config.Headers,
103+
next: { revalidate: ttlSeconds },
104+
}),
105+
]);
106+
107+
if (!mlRes.ok) {
91108
console.warn(
92-
`Menulinks API returned ${res.status} ${res.statusText} — falling back to static navigation`,
109+
`Menulinks API returned ${mlRes.status} ${mlRes.statusText} — falling back to static navigation`,
93110
menulinksUrl
94111
);
95112
return loadFallbackNavSections(options.currentSlug);
96113
}
97114

98-
const json: unknown = await res.json();
115+
const json: unknown = await mlRes.json();
99116

100117
const apiTree = navPayloadToApiNavTree(json, folderPath);
101118

119+
if (navRes.ok) {
120+
try {
121+
const navJson: unknown = await navRes.json();
122+
const overlay = extractNavApiFolderOverlay(navJson, folderPath);
123+
applyNavFolderOverlayToMenulinksTree(apiTree, overlay);
124+
resortMenulinksDerivedTree(apiTree);
125+
} catch (navErr) {
126+
console.warn("Nav folder overlay failed — using menulinks-only folder order/titles", navErr);
127+
}
128+
} else {
129+
console.warn(
130+
`Nav API returned ${navRes.status} ${navRes.statusText} — menulinks-only folder order/titles`,
131+
navUrl
132+
);
133+
}
134+
102135
if (!sectionsAllForPaths?.length) {
103136
sectionsAllForPaths = transformApiResponseToNavSections(
104137
filterApiNavKeepAllLeaves(apiTree)

util/config.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,18 @@ export const Config = {
3131
NavFolderPath: navFolderPath,
3232
/** Menulinks `depth` (default 2 to match typical plugin queries). */
3333
NavMenuDepth: Math.max(1, Number(process.env.NEXT_PUBLIC_DOTCMS_NAV_MENU_DEPTH ?? 2) || 2),
34+
/**
35+
* `GET /api/v1/nav` depth for the folder overlay only (order + titles on synthetic `.nav.*` folders).
36+
* DotCMS counts the **requested folder as depth 1**, then each step outward is +1: depth 2 is
37+
* immediate children of `/docs/nav`, depth 3 is **grandchildren** (the second tier of section
38+
* folders, e.g. under “Getting Started”). Two UI “folder rows” under the nav root therefore need **3** here.
39+
* Deeper values add payload (more links under expanded nodes) for little gain unless the tree grows.
40+
* Override with `NEXT_PUBLIC_DOTCMS_NAV_FOLDER_OVERLAY_DEPTH`.
41+
*/
42+
NavFolderOverlayDepth: Math.max(
43+
1,
44+
Number(process.env.NEXT_PUBLIC_DOTCMS_NAV_FOLDER_OVERLAY_DEPTH ?? 3) || 3
45+
),
3446
GraphqlUrl: process.env.NEXT_PUBLIC_API_GRAPH_URL || ((normalizedDotCMSHost + '/api/v1/graphql') as string),
3547
AuthToken: process.env.NEXT_PUBLIC_DOTCMS_AUTH_TOKEN as string,
3648
SwaggerUrl: ((process.env.NEXT_PUBLIC_API_SWAGGER_URL || normalizedDotCMSHost) + '/api/openapi.json') as string,

util/menulinksToApiNav.ts

Lines changed: 117 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -177,16 +177,129 @@ function getOrCreateFolderChild(parent: ApiNavItem, segmentKey: string, orderHin
177177
return f;
178178
}
179179

180+
function compareNavSiblings(a: ApiNavItem, b: ApiNavItem): number {
181+
if (a.order !== b.order) return a.order - b.order;
182+
return a.title.localeCompare(b.title);
183+
}
184+
180185
function sortTreeRecursive(node: ApiNavItem): void {
181-
node.children.sort((a, b) => {
182-
if (a.order !== b.order) return a.order - b.order;
183-
return a.title.localeCompare(b.title);
184-
});
186+
node.children.sort(compareNavSiblings);
185187
for (const c of node.children) {
186188
if (c.type === "folder") sortTreeRecursive(c);
187189
}
188190
}
189191

192+
/** Re-sort siblings after mutating folder `order` (e.g. Nav API overlay). */
193+
export function resortMenulinksDerivedTree(items: ApiNavItem[]): void {
194+
items.sort(compareNavSiblings);
195+
for (const c of items) {
196+
if (c.type === "folder") sortTreeRecursive(c);
197+
}
198+
}
199+
200+
export type NavFolderOverlayEntry = { order: number; title: string };
201+
202+
/** Path under `navRoot`: `development/apis` (no leading slash, lowercased). */
203+
export function navHrefToFolderOverlayKey(
204+
href: string | undefined,
205+
navRoot: string
206+
): string | null {
207+
const h = href?.trim();
208+
if (!h) return null;
209+
let pathname = h;
210+
if (/^https?:\/\//i.test(h) || h.startsWith("//")) {
211+
try {
212+
pathname = new URL(h.startsWith("//") ? `https:${h}` : h).pathname;
213+
} catch {
214+
return null;
215+
}
216+
}
217+
const p = pathname.replace(/\/+$/, "");
218+
const root = navRoot.trim().replace(/\/+$/, "");
219+
const normRoot = root.toLowerCase();
220+
const idx = p.toLowerCase().indexOf(normRoot);
221+
if (idx === -1) return null;
222+
const rest = p.slice(idx + root.length).replace(/^\/+/, "").replace(/\/+$/, "");
223+
return rest.length > 0 ? rest.toLowerCase() : null;
224+
}
225+
226+
function collectNavFolderOverlay(
227+
nodes: ApiNavItem[] | undefined,
228+
navRoot: string,
229+
out: Map<string, NavFolderOverlayEntry>
230+
): void {
231+
if (!nodes?.length) return;
232+
for (const n of nodes) {
233+
if (n.type === "folder") {
234+
const key = navHrefToFolderOverlayKey(n.href, navRoot);
235+
if (key) {
236+
out.set(key, {
237+
order: Number(n.order) || 0,
238+
title: (n.title ?? "").trim(),
239+
});
240+
}
241+
collectNavFolderOverlay(n.children, navRoot, out);
242+
}
243+
}
244+
}
245+
246+
/**
247+
* Folder path → CMS `order` and display title from `GET /api/v1/nav` (menulinks-only trees lack
248+
* Nav Tool folder metadata; synthetic titles would otherwise come from slug segments, e.g. "Apis").
249+
*/
250+
export function extractNavApiFolderOverlay(
251+
navApiJson: unknown,
252+
navRoot: string
253+
): Map<string, NavFolderOverlayEntry> {
254+
const out = new Map<string, NavFolderOverlayEntry>();
255+
if (!navApiJson || typeof navApiJson !== "object") return out;
256+
const children = (navApiJson as { entity?: { children?: ApiNavItem[] } }).entity?.children;
257+
if (Array.isArray(children)) {
258+
collectNavFolderOverlay(children, navRoot, out);
259+
}
260+
return out;
261+
}
262+
263+
/** Mutates menulinks-built folders whose `code` is `.nav.<segment>` (see {@link folderMarker}). */
264+
export function applyNavFolderOverlayToMenulinksTree(
265+
items: ApiNavItem[],
266+
overlay: Map<string, NavFolderOverlayEntry>
267+
): void {
268+
const walk = (nodes: ApiNavItem[], pathPrefix: string): void => {
269+
for (const node of nodes) {
270+
if (node.type !== "folder") continue;
271+
const code = node.code ?? "";
272+
if (!code.startsWith(".nav.")) {
273+
walk(node.children ?? [], pathPrefix);
274+
continue;
275+
}
276+
const seg = code.slice(".nav.".length);
277+
const key = pathPrefix ? `${pathPrefix}/${seg}` : seg;
278+
const low = key.toLowerCase();
279+
const meta = overlay.get(low);
280+
if (meta) {
281+
node.order = meta.order;
282+
if (meta.title) node.title = meta.title;
283+
}
284+
walk(node.children ?? [], low);
285+
}
286+
};
287+
walk(items, "");
288+
}
289+
290+
/** Same path pattern as legacy `getNavSections`: `/api/v1/nav/docs/nav?depth=…&languageId=…`. */
291+
export function buildNavApiUrl(
292+
dotcmsHost: string,
293+
navPath: string,
294+
depth: number,
295+
languageId: number
296+
): string {
297+
const base = dotcmsHost.replace(/\/+$/, "");
298+
const p = navPath.startsWith("/") ? navPath : `/${navPath}`;
299+
const d = Math.max(1, depth);
300+
return `${base}/api/v1/nav${p}?depth=${d}&languageId=${languageId}`;
301+
}
302+
190303
/**
191304
* Build top-level section folders (ApiNavItem tree) from flat menulinks rows.
192305
*/

util/page.utils.js

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import { Config } from "./config";
22
import { client } from "./dotcmsClient";
33
import { navCache, getCacheKey } from "./cacheService";
4-
import { buildMenulinksUrl, navPayloadToApiNavTree } from "./menulinksToApiNav";
4+
import {
5+
applyNavFolderOverlayToMenulinksTree,
6+
buildMenulinksUrl,
7+
buildNavApiUrl,
8+
extractNavApiFolderOverlay,
9+
navPayloadToApiNavTree,
10+
resortMenulinksDerivedTree,
11+
} from "./menulinksToApiNav";
512
import { filterApiNavForMenuAndSlug, filterApiNavKeepAllLeaves } from "./navTransform";
613

714
export const fetchPageData = async (params) => {
@@ -27,26 +34,45 @@ export const fetchNavData = async (dataIn) => {
2734
const depth = dataIn.depth ?? Config.NavMenuDepth;
2835
const navMenuSlug = dataIn.navMenuSlug ?? dataIn.currentSlug;
2936

37+
const navOverlayDepth = Config.NavFolderOverlayDepth;
38+
const languageId = Config.LanguageId ?? 1;
3039
const rawCacheKey = getCacheKey(
31-
`menulinks|${Config.NavSiteId}|${folderPath}|${depth}|tree`
40+
`menulinks|${Config.NavSiteId}|${folderPath}|${depth}|nav${navOverlayDepth}|tree`
3241
);
3342

3443
let tree = navCache.get(rawCacheKey);
3544

3645
const menulinksUrl = buildMenulinksUrl(Config.DotCMSHost, Config.NavSiteId, folderPath, depth);
46+
const navUrl = buildNavApiUrl(Config.DotCMSHost, folderPath, navOverlayDepth, languageId);
3747

3848
try {
3949
if (!tree) {
40-
const res = await fetch(menulinksUrl, {
41-
method: "GET",
42-
headers: Config.Headers,
43-
});
44-
if (!res.ok) {
45-
console.warn("Menulinks fetch failed:", res.status, menulinksUrl);
50+
const [mlRes, navRes] = await Promise.all([
51+
fetch(menulinksUrl, {
52+
method: "GET",
53+
headers: Config.Headers,
54+
}),
55+
fetch(navUrl, {
56+
method: "GET",
57+
headers: Config.Headers,
58+
}),
59+
]);
60+
if (!mlRes.ok) {
61+
console.warn("Menulinks fetch failed:", mlRes.status, menulinksUrl);
4662
return { nav: null, navAllForPaths: null };
4763
}
48-
const json = await res.json();
64+
const json = await mlRes.json();
4965
tree = navPayloadToApiNavTree(json, folderPath);
66+
if (navRes.ok) {
67+
try {
68+
const navJson = await navRes.json();
69+
const overlay = extractNavApiFolderOverlay(navJson, folderPath);
70+
applyNavFolderOverlayToMenulinksTree(tree, overlay);
71+
resortMenulinksDerivedTree(tree);
72+
} catch (navErr) {
73+
console.warn("Nav folder overlay failed — using menulinks-only folders", navErr);
74+
}
75+
}
5076
if (tree && tree.length > 0) {
5177
navCache.set(rawCacheKey, tree, cacheTTL);
5278
}

0 commit comments

Comments
 (0)