@morphika/andami 0.8.1 → 0.8.3

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.
@@ -7,6 +7,7 @@ import type { Page } from "../../../lib/sanity/types";
7
7
  import { PageRenderer } from "../../../components/blocks";
8
8
  import { getSiteConfig } from "../../../lib/config";
9
9
  import { assetUrl } from "../../../lib/assets";
10
+ import BreadcrumbJsonLd from "../../../components/seo/BreadcrumbJsonLd";
10
11
 
11
12
  const cfg = getSiteConfig();
12
13
 
@@ -83,6 +84,12 @@ export default async function DynamicPage({ params }: PageProps) {
83
84
 
84
85
  return (
85
86
  <main className="min-h-screen">
87
+ <BreadcrumbJsonLd
88
+ items={[
89
+ { name: "Home", path: "/" },
90
+ { name: page.title },
91
+ ]}
92
+ />
86
93
  <PageRenderer page={page} />
87
94
  </main>
88
95
  );
@@ -8,6 +8,7 @@ import { PageRenderer } from "../../../../components/blocks";
8
8
  import { getSiteConfig } from "../../../../lib/config";
9
9
  import { assetUrl } from "../../../../lib/assets";
10
10
  import ProjectJsonLd from "../../../../components/seo/ProjectJsonLd";
11
+ import BreadcrumbJsonLd from "../../../../components/seo/BreadcrumbJsonLd";
11
12
 
12
13
  const cfg = getSiteConfig();
13
14
 
@@ -85,6 +86,13 @@ export default async function ProjectPage({ params }: ProjectPageProps) {
85
86
  return (
86
87
  <main className="min-h-screen">
87
88
  <ProjectJsonLd page={page} />
89
+ <BreadcrumbJsonLd
90
+ items={[
91
+ { name: "Home", path: "/" },
92
+ { name: "Work", path: "/work" },
93
+ { name: page.title },
94
+ ]}
95
+ />
88
96
  <PageRenderer page={page} />
89
97
  </main>
90
98
  );
@@ -96,8 +96,12 @@ export async function GET() {
96
96
  lines.push("**Social profiles:**");
97
97
  for (const link of settings.social_links) {
98
98
  if (!link.url) continue;
99
- const label = link.label ? link.label : link.url;
100
- lines.push(`- ${label}: ${link.url}`);
99
+ const label = link.label?.trim();
100
+ // Emit "Label: URL" only when a meaningful label is present (and is not
101
+ // just a duplicate of the URL). Otherwise emit the URL alone — avoids
102
+ // the "URL: URL" duplication when a user types the URL into both fields.
103
+ const showLabel = label && label !== link.url;
104
+ lines.push(showLabel ? `- ${label}: ${link.url}` : `- ${link.url}`);
101
105
  }
102
106
  lines.push("");
103
107
  }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * BreadcrumbJsonLd — Server component that emits a `BreadcrumbList` JSON-LD
3
+ * record for nested pages. Helps Google understand site hierarchy and can
4
+ * replace the plain URL in SERP with visual breadcrumbs.
5
+ *
6
+ * Accepts items as relative paths (e.g. "/work") — the component resolves
7
+ * them against the configured domain so callers don't have to construct
8
+ * absolute URLs themselves. The last item typically omits `path` to indicate
9
+ * the current page.
10
+ *
11
+ * Example (project page):
12
+ * <BreadcrumbJsonLd items={[
13
+ * { name: "Home", path: "/" },
14
+ * { name: "Work", path: "/work" },
15
+ * { name: project.title },
16
+ * ]} />
17
+ *
18
+ * Returns null (via the underlying builder) when fewer than 2 items are
19
+ * supplied — a single-item breadcrumb is not a breadcrumb.
20
+ */
21
+
22
+ import JsonLd from "./JsonLd";
23
+ import { buildBreadcrumbJsonLd } from "../../lib/seo/jsonld";
24
+ import { getSiteConfig } from "../../lib/config";
25
+
26
+ interface BreadcrumbJsonLdItem {
27
+ /** Display name of the crumb. */
28
+ name: string;
29
+ /** Relative path (e.g. "/work"). Omit for the current page (last item). */
30
+ path?: string;
31
+ }
32
+
33
+ interface BreadcrumbJsonLdProps {
34
+ items: BreadcrumbJsonLdItem[];
35
+ }
36
+
37
+ export default function BreadcrumbJsonLd({ items }: BreadcrumbJsonLdProps) {
38
+ const cfg = getSiteConfig();
39
+ const baseUrl = cfg.domain.replace(/\/$/, "");
40
+
41
+ const ld = buildBreadcrumbJsonLd({
42
+ items: items.map((item) => ({
43
+ name: item.name,
44
+ url: item.path ? `${baseUrl}${item.path.startsWith("/") ? "" : "/"}${item.path}` : undefined,
45
+ })),
46
+ });
47
+
48
+ return <JsonLd data={ld} />;
49
+ }
package/lib/seo/jsonld.ts CHANGED
@@ -60,6 +60,18 @@ export interface CreativeWorkInput {
60
60
  authorName?: string;
61
61
  }
62
62
 
63
+ export interface BreadcrumbItem {
64
+ /** Display name of the crumb (e.g. "Work", "About"). */
65
+ name: string;
66
+ /** Absolute URL of the crumb's page. Omit for the current page (last item). */
67
+ url?: string;
68
+ }
69
+
70
+ export interface BreadcrumbInput {
71
+ /** Ordered list from root → current. First item is typically "Home". */
72
+ items: BreadcrumbItem[];
73
+ }
74
+
63
75
  // ============================================
64
76
  // Builders
65
77
  // ============================================
@@ -172,3 +184,36 @@ export function buildCreativeWorkJsonLd(
172
184
 
173
185
  return ld;
174
186
  }
187
+
188
+ /**
189
+ * BreadcrumbList — emitted on nested pages (project detail, regular pages
190
+ * below the homepage). Helps Google understand site hierarchy and can replace
191
+ * the plain URL in SERP with visual breadcrumbs (`morphika.tv › Work › Udon`).
192
+ *
193
+ * Per schema.org guidance, the last item (the current page) typically omits
194
+ * the `item` URL — the trailing item is the page the breadcrumb is rendered
195
+ * on, so the URL is redundant.
196
+ *
197
+ * Returns null when fewer than 2 items are supplied (a single-item breadcrumb
198
+ * is not a breadcrumb).
199
+ */
200
+ export function buildBreadcrumbJsonLd(
201
+ input: BreadcrumbInput
202
+ ): Record<string, unknown> | null {
203
+ const items = (input.items || []).filter((i) => i && i.name);
204
+ if (items.length < 2) return null;
205
+
206
+ return {
207
+ "@context": "https://schema.org",
208
+ "@type": "BreadcrumbList",
209
+ itemListElement: items.map((item, index) => {
210
+ const listItem: Record<string, unknown> = {
211
+ "@type": "ListItem",
212
+ position: index + 1,
213
+ name: item.name,
214
+ };
215
+ if (item.url) listItem.item = item.url;
216
+ return listItem;
217
+ }),
218
+ };
219
+ }
package/lib/version.ts CHANGED
@@ -6,4 +6,4 @@
6
6
  * Exposed as a plain constant so it can be imported without reading
7
7
  * package.json at runtime.
8
8
  */
9
- export const ANDAMI_VERSION = "0.8.1";
9
+ export const ANDAMI_VERSION = "0.8.3";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morphika/andami",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
4
4
  "description": "Visual Page Builder — core library. A reusable website builder with visual editing, CMS integration, and asset management.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/site/llms-txt.ts CHANGED
@@ -2,9 +2,22 @@
2
2
  * @morphika/andami/site/llms-txt — AI/LLM-friendly site summary route.
3
3
  *
4
4
  * Re-exports the framework's /llms.txt route handler so instances can mount
5
- * it at `/llms.txt`. Required instance setup (one-time, see CHANGELOG):
5
+ * it at `/llms.txt`.
6
+ *
7
+ * Required instance setup (one-time):
6
8
  *
7
9
  * // app/llms.txt/route.ts (in your instance repo)
8
- * export { GET, revalidate } from "@morphika/andami/site/llms-txt";
10
+ * import "@/lib/config-init";
11
+ *
12
+ * // IMPORTANT: declare revalidate locally — Next.js requires
13
+ * // statically-parseable route segment config and rejects re-exports.
14
+ * export const revalidate = 86400;
15
+ *
16
+ * export { GET } from "@morphika/andami/site/llms-txt";
17
+ *
18
+ * Note: `revalidate` is re-exported from this barrel only as a convenience
19
+ * value for instances that want to reference the same default — it must
20
+ * NOT be re-exported via `export { revalidate } from ...` inside the
21
+ * instance's route file (Turbopack/Next will fail the build).
9
22
  */
10
23
  export { GET, revalidate } from "../app/llms.txt/route";