@morphika/andami 0.8.1 → 0.8.5
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/app/(site)/[slug]/page.tsx +7 -0
- package/app/(site)/work/[slug]/page.tsx +8 -0
- package/app/layout.tsx +36 -19
- package/app/llms.txt/route.ts +6 -2
- package/components/seo/BreadcrumbJsonLd.tsx +49 -0
- package/lib/seo/favicon.ts +20 -0
- package/lib/seo/jsonld.ts +45 -0
- package/lib/version.ts +1 -1
- package/package.json +2 -1
- package/site/llms-txt.ts +15 -2
|
@@ -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
|
);
|
package/app/layout.tsx
CHANGED
|
@@ -2,6 +2,9 @@ import type { Metadata } from "next";
|
|
|
2
2
|
import { VisualEditing } from "next-sanity/visual-editing";
|
|
3
3
|
import { draftMode } from "next/headers";
|
|
4
4
|
import { registerConfig, getSiteConfig } from "../lib/config";
|
|
5
|
+
import { getCachedSiteSettings } from "../lib/seo/site-settings";
|
|
6
|
+
import { assetUrl } from "../lib/assets";
|
|
7
|
+
import { buildFaviconMetadata } from "../lib/seo/favicon";
|
|
5
8
|
import siteConfig from "../site.config";
|
|
6
9
|
import "./globals.css";
|
|
7
10
|
|
|
@@ -12,26 +15,40 @@ registerConfig(siteConfig);
|
|
|
12
15
|
|
|
13
16
|
const cfg = getSiteConfig();
|
|
14
17
|
|
|
15
|
-
export const metadata
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
18
|
+
// Use generateMetadata (dynamic) instead of `export const metadata` (static)
|
|
19
|
+
// so we can read `siteSettings.favicon_path` from Sanity per render. The
|
|
20
|
+
// underlying `getCachedSiteSettings()` is React-cache-dedup'd across the same
|
|
21
|
+
// render pass, so SiteSeoHead (which also reads settings) shares the fetch.
|
|
22
|
+
// With ISR enabled on pages, this effectively means one Sanity read per page
|
|
23
|
+
// per revalidation window — negligible cost.
|
|
24
|
+
export async function generateMetadata(): Promise<Metadata> {
|
|
25
|
+
const settings = await getCachedSiteSettings();
|
|
26
|
+
const faviconUrl = settings?.favicon_path
|
|
27
|
+
? assetUrl(settings.favicon_path)
|
|
28
|
+
: undefined;
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
title: {
|
|
32
|
+
default: cfg.defaults.metaTitle,
|
|
33
|
+
template: `%s — ${cfg.name}`,
|
|
34
|
+
},
|
|
32
35
|
description: cfg.defaults.metaDescription,
|
|
33
|
-
|
|
34
|
-
|
|
36
|
+
metadataBase: new URL(cfg.domain),
|
|
37
|
+
icons: buildFaviconMetadata(faviconUrl),
|
|
38
|
+
openGraph: {
|
|
39
|
+
title: cfg.defaults.metaTitle,
|
|
40
|
+
description: cfg.defaults.metaDescription,
|
|
41
|
+
siteName: cfg.name,
|
|
42
|
+
type: "website",
|
|
43
|
+
url: cfg.domain,
|
|
44
|
+
},
|
|
45
|
+
twitter: {
|
|
46
|
+
card: "summary_large_image",
|
|
47
|
+
title: cfg.defaults.metaTitle,
|
|
48
|
+
description: cfg.defaults.metaDescription,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
35
52
|
|
|
36
53
|
export default async function RootLayout({
|
|
37
54
|
children,
|
package/app/llms.txt/route.ts
CHANGED
|
@@ -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
|
|
100
|
-
|
|
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
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Favicon — builder for the Next.js Metadata.icons field.
|
|
3
|
+
*
|
|
4
|
+
* The favicon URL should be pre-resolved to an absolute or root-relative
|
|
5
|
+
* URL by the caller (e.g. via assetUrl()) — this helper just shapes the
|
|
6
|
+
* Metadata.icons value and handles the empty case.
|
|
7
|
+
*
|
|
8
|
+
* Returns undefined when no URL is supplied so callers can spread the
|
|
9
|
+
* result into a Metadata object without leaving an empty `icons` field
|
|
10
|
+
* (Next.js would otherwise emit a default favicon hint).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { Metadata } from "next";
|
|
14
|
+
|
|
15
|
+
export function buildFaviconMetadata(
|
|
16
|
+
faviconUrl: string | null | undefined
|
|
17
|
+
): Metadata["icons"] | undefined {
|
|
18
|
+
if (!faviconUrl) return undefined;
|
|
19
|
+
return { icon: faviconUrl };
|
|
20
|
+
}
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morphika/andami",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.5",
|
|
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",
|
|
@@ -82,6 +82,7 @@
|
|
|
82
82
|
"./lib/editor": "./lib/editor/index.ts",
|
|
83
83
|
"./lib/editor/*": "./lib/editor/*.ts",
|
|
84
84
|
"./lib/animation/*": "./lib/animation/*.ts",
|
|
85
|
+
"./lib/seo/*": "./lib/seo/*.ts",
|
|
85
86
|
"./lib/shader/glsl": "./lib/shader/glsl/index.ts",
|
|
86
87
|
"./lib/backup/manifest": "./lib/backup/manifest.ts",
|
|
87
88
|
"./lib/backup/export": "./lib/backup/export.ts",
|
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`.
|
|
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
|
-
*
|
|
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";
|