@estation/create-cms-site 3.0.1 → 3.0.2

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": "@estation/create-cms-site",
3
- "version": "3.0.1",
3
+ "version": "3.0.2",
4
4
  "description": "MCP server to scaffold Next.js sites powered by eSTATION CMS",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,64 +1,75 @@
1
1
  "use client";
2
2
 
3
3
  import { usePathname } from "next/navigation";
4
+ import { useEffect, useState } from "react";
5
+
6
+ interface Crumb {
7
+ label: string;
8
+ href: string;
9
+ }
10
+
11
+ const HOME_LABELS: Record<string, string> = {
12
+ en: "Home", ar: "الرئيسية", ku: "سەرەکی", fr: "Accueil",
13
+ de: "Startseite", es: "Inicio", tr: "Ana Sayfa", it: "Home",
14
+ pt: "Início", zh: "首页", ja: "ホーム", ko: "홈",
15
+ };
4
16
 
5
17
  /**
6
- * Breadcrumbs component for website pages.
7
- * Auto-generates breadcrumb trail from the URL path.
18
+ * Auto breadcrumbs reads URL, fetches localized titles from CMS.
19
+ * No props needed place once in the layout, works everywhere.
8
20
  *
9
- * Examples:
10
- * /en → (hidden homepage)
11
- * /en/about-us Home > About Us
12
- * /en/partners → Home > Partners
13
- * /en/partners/york → Home > Partners > York
21
+ * /en → (hidden)
22
+ * /en/about-us Home > About Us (from CMS page title)
23
+ * /ar/partners/york الرئيسية / شركاؤنا / يورك
14
24
  */
15
25
  export function Breadcrumbs() {
16
26
  const pathname = usePathname();
17
- if (!pathname) return null;
18
-
19
- // Split path: /en/partners/york → ["en", "partners", "york"]
20
- const segments = pathname.split("/").filter(Boolean);
21
-
22
- // First segment is locale — skip it
23
- const locale = segments[0] || "en";
24
- const pathSegments = segments.slice(1);
25
-
26
- // Don't show breadcrumbs on homepage
27
- if (pathSegments.length === 0) return null;
28
-
29
- // Build breadcrumb items
30
- const crumbs: { label: string; href: string }[] = [
31
- { label: "Home", href: `/${locale}` },
32
- ];
33
-
34
- let currentPath = `/${locale}`;
35
- for (const segment of pathSegments) {
36
- currentPath += `/${segment}`;
37
- // Convert slug to display name: "about-us" "About Us", "york" → "York"
38
- const label = segment
39
- .split("-")
40
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
41
- .join(" ");
42
- crumbs.push({ label, href: currentPath });
43
- }
27
+ const [crumbs, setCrumbs] = useState<Crumb[]>([]);
28
+
29
+ useEffect(() => {
30
+ if (!pathname) return;
31
+
32
+ const segments = pathname.split("/").filter(Boolean);
33
+ const locale = segments[0] || "en";
34
+ const pathSegments = segments.slice(1);
35
+
36
+ if (pathSegments.length === 0) {
37
+ setCrumbs([]);
38
+ return;
39
+ }
40
+
41
+ const homeCrumb: Crumb = {
42
+ label: HOME_LABELS[locale] || "Home",
43
+ href: `/${locale}`,
44
+ };
45
+
46
+ // Show slug-based labels immediately
47
+ const initial: Crumb[] = [homeCrumb];
48
+ let p = `/${locale}`;
49
+ for (const seg of pathSegments) {
50
+ p += `/${seg}`;
51
+ initial.push({ label: slugToTitle(seg), href: p });
52
+ }
53
+ setCrumbs(initial);
54
+
55
+ // Then fetch real titles from CMS
56
+ fetchTitles(locale, pathSegments).then((fetched) => {
57
+ if (fetched.length > 0) setCrumbs([homeCrumb, ...fetched]);
58
+ });
59
+ }, [pathname]);
60
+
61
+ if (crumbs.length === 0) return null;
44
62
 
45
63
  return (
46
- <nav aria-label="Breadcrumb" className="px-6 py-3">
47
- <ol className="flex items-center gap-1.5 text-sm text-gray-500">
64
+ <nav aria-label="Breadcrumb" className="px-6 py-3 max-w-6xl mx-auto">
65
+ <ol className="flex items-center gap-1.5 text-sm text-gray-500 flex-wrap">
48
66
  {crumbs.map((crumb, idx) => (
49
67
  <li key={crumb.href} className="flex items-center gap-1.5">
50
- {idx > 0 && (
51
- <span className="text-gray-300" aria-hidden="true">/</span>
52
- )}
68
+ {idx > 0 && <span className="text-gray-300">/</span>}
53
69
  {idx === crumbs.length - 1 ? (
54
70
  <span className="text-gray-900 font-medium">{crumb.label}</span>
55
71
  ) : (
56
- <a
57
- href={crumb.href}
58
- className="hover:text-gray-700 hover:underline transition-colors"
59
- >
60
- {crumb.label}
61
- </a>
72
+ <a href={crumb.href} className="hover:text-gray-700 hover:underline transition-colors">{crumb.label}</a>
62
73
  )}
63
74
  </li>
64
75
  ))}
@@ -66,3 +77,43 @@ export function Breadcrumbs() {
66
77
  </nav>
67
78
  );
68
79
  }
80
+
81
+ async function fetchTitles(locale: string, segments: string[]): Promise<Crumb[]> {
82
+ const apiUrl = process.env.NEXT_PUBLIC_CMS_API_URL || "https://cms-gateway.estation.io/api/v1";
83
+ const token = process.env.NEXT_PUBLIC_CMS_API_TOKEN || "";
84
+ const v2 = apiUrl.replace(/\/api\/v[12]$/, "/api/v2");
85
+ const crumbs: Crumb[] = [];
86
+ let path = `/${locale}`;
87
+
88
+ for (let i = 0; i < segments.length; i++) {
89
+ path += `/${segments[i]}`;
90
+ let label = slugToTitle(segments[i]);
91
+
92
+ try {
93
+ if (i === 0) {
94
+ const r = await fetch(`${apiUrl}/public/content/pages/slug/${segments[i]}?locale=${locale}`, { headers: { "X-API-TOKEN": token }, cache: "force-cache" });
95
+ if (r.ok) {
96
+ const j = await r.json();
97
+ const pg = j.data?.page || j.page;
98
+ if (pg) label = pg.metadata?.titles?.[locale] || pg.title || label;
99
+ }
100
+ } else if (i === 1) {
101
+ const type = deriveType(segments[0]);
102
+ const r = await fetch(`${v2}/public/content/${type}/slug/${segments[i]}`, { headers: { "X-API-TOKEN": token }, cache: "force-cache" });
103
+ if (r.ok) {
104
+ const j = await r.json();
105
+ const b = j.data || j;
106
+ const fv = b?.content?.title?.fieldValue;
107
+ if (fv) label = typeof fv === "string" ? fv : (fv[locale] || fv.en || Object.values(fv)[0] as string || label);
108
+ else if (b?.name) label = b.name;
109
+ }
110
+ }
111
+ } catch { /* fallback */ }
112
+
113
+ crumbs.push({ label, href: path });
114
+ }
115
+ return crumbs;
116
+ }
117
+
118
+ function slugToTitle(s: string) { return s.split("-").map(w => w[0].toUpperCase() + w.slice(1)).join(" "); }
119
+ function deriveType(s: string) { return s.endsWith("ies") ? s.slice(0,-3)+"y" : s.endsWith("s") ? s.slice(0,-1) : s; }