@morphika/andami 0.5.11 → 0.8.1

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.
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Cached siteSettings fetch — shared between (site)/layout.tsx and the
3
+ * SEO server components (SiteSeoHead, ProjectJsonLd).
4
+ *
5
+ * React's `cache()` deduplicates within a single render pass: layout and
6
+ * SEO components called from the same route render share one Sanity fetch.
7
+ * The underlying `client` already uses Sanity's CDN (2ms vs 200ms), so
8
+ * cold fetches are cheap anyway.
9
+ */
10
+
11
+ import { cache } from "react";
12
+ import { client } from "../sanity/client";
13
+ import { siteSettingsQuery } from "../sanity/queries";
14
+ import type { SiteSettings } from "../sanity/types";
15
+
16
+ export const getCachedSiteSettings = cache(async (): Promise<SiteSettings | null> => {
17
+ try {
18
+ return await client.fetch<SiteSettings>(siteSettingsQuery);
19
+ } catch (error) {
20
+ console.error("[SEO] Failed to fetch site settings:", error);
21
+ return null;
22
+ }
23
+ });
24
+
25
+ /**
26
+ * Coerce any asset URL (relative path, /api proxy, or full CDN URL) to an
27
+ * absolute URL using the configured site domain as base. Required by
28
+ * schema.org — JSON-LD URLs must be fully qualified.
29
+ */
30
+ export function toAbsoluteUrl(url: string | undefined, base: string): string | undefined {
31
+ if (!url) return undefined;
32
+ try {
33
+ return new URL(url, base).toString();
34
+ } catch {
35
+ return undefined;
36
+ }
37
+ }
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.5.11";
9
+ export const ANDAMI_VERSION = "0.8.1";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morphika/andami",
3
- "version": "0.5.11",
3
+ "version": "0.8.1",
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",
@@ -128,6 +128,7 @@
128
128
  "./site/error": "./site/error.ts",
129
129
  "./site/robots": "./site/robots.ts",
130
130
  "./site/sitemap": "./site/sitemap.ts",
131
+ "./site/llms-txt": "./site/llms-txt.ts",
131
132
  "./admin": "./admin/index.ts",
132
133
  "./admin/login": "./admin/login.ts",
133
134
  "./admin/pages": "./admin/pages.ts",
@@ -66,6 +66,13 @@ export default defineType({
66
66
  type: "string",
67
67
  description: "Relative path to the Open Graph image",
68
68
  }),
69
+ defineField({
70
+ name: "noindex",
71
+ title: "Hide from Search Engines",
72
+ type: "boolean",
73
+ description: "When enabled, this page is excluded from sitemap.xml and emits robots: noindex,nofollow. Use for thank-you pages, drafts, internal-only routes.",
74
+ initialValue: false,
75
+ }),
69
76
  ],
70
77
  }),
71
78
  defineField({
@@ -11,6 +11,9 @@ export default defineType({
11
11
  { name: "nav", title: "Navigation", default: true },
12
12
  { name: "nav_design", title: "Nav Design" },
13
13
  { name: "meta", title: "Metadata" },
14
+ { name: "social", title: "Social & Branding" },
15
+ { name: "organization", title: "Organization Info" },
16
+ { name: "verification", title: "Search Verification" },
14
17
  { name: "assets", title: "Assets" },
15
18
  ],
16
19
  fields: [
@@ -296,6 +299,105 @@ export default defineType({
296
299
  defineField({ name: "favicon_path", title: "Favicon Path", type: "string", group: "meta" }),
297
300
  defineField({ name: "analytics_id", title: "Analytics ID", type: "string", group: "meta" }),
298
301
 
302
+ // === SOCIAL & BRANDING (used by JSON-LD structured data) ===
303
+ defineField({
304
+ name: "org_logo",
305
+ title: "Organization Logo",
306
+ type: "string",
307
+ group: "social",
308
+ description: "Asset path to the logo used by Google's Knowledge Panel and rich results. Square images recommended (min 112×112, ideal 500×500).",
309
+ }),
310
+ defineField({
311
+ name: "default_author",
312
+ title: "Default Author",
313
+ type: "string",
314
+ group: "social",
315
+ description: "Used as the author of project pages when not overridden. Typically your studio/agency name.",
316
+ }),
317
+ defineField({
318
+ name: "social_links",
319
+ title: "Social Profiles",
320
+ type: "array",
321
+ group: "social",
322
+ description: "Profile URLs (Instagram, LinkedIn, Behance, Vimeo, etc.) emitted as schema.org sameAs[] so Google links your organization to its social presence.",
323
+ of: [
324
+ {
325
+ type: "object",
326
+ fields: [
327
+ defineField({ name: "label", title: "Label", type: "string", description: "Optional display name (e.g. Instagram, Behance). For admin organization only." }),
328
+ defineField({ name: "url", title: "URL", type: "url", validation: (Rule) => Rule.required().uri({ scheme: ["http", "https"] }) }),
329
+ ],
330
+ preview: {
331
+ select: { label: "label", url: "url" },
332
+ prepare({ label, url }: { label?: string; url?: string }) {
333
+ return {
334
+ title: label || url || "(empty)",
335
+ subtitle: label ? url : undefined,
336
+ };
337
+ },
338
+ },
339
+ },
340
+ ],
341
+ }),
342
+
343
+ // === ORGANIZATION INFO (citation-friendly facts for LLMs + JSON-LD) ===
344
+ defineField({
345
+ name: "founding_year",
346
+ title: "Founded (year)",
347
+ type: "number",
348
+ group: "organization",
349
+ description: "Year your organization was founded. Cited by AI agents answering 'when was X founded'. Emitted as schema.org foundingDate.",
350
+ validation: (Rule) => Rule.min(1800).max(new Date().getFullYear()),
351
+ }),
352
+ defineField({
353
+ name: "address_locality",
354
+ title: "City",
355
+ type: "string",
356
+ group: "organization",
357
+ description: "City or town where your organization is based (e.g. 'Barcelona', 'Berlin'). Emitted as schema.org address.addressLocality.",
358
+ }),
359
+ defineField({
360
+ name: "address_country",
361
+ title: "Country",
362
+ type: "string",
363
+ group: "organization",
364
+ description: "Country name or ISO code (e.g. 'Spain', 'ES'). Emitted as schema.org address.addressCountry.",
365
+ }),
366
+ defineField({
367
+ name: "contact_email",
368
+ title: "Contact Email",
369
+ type: "string",
370
+ group: "organization",
371
+ description: "Public contact email. Emitted as schema.org contactPoint.email.",
372
+ validation: (Rule) => Rule.email().error("Must be a valid email address"),
373
+ }),
374
+ defineField({
375
+ name: "keywords",
376
+ title: "Areas of Expertise",
377
+ type: "array",
378
+ group: "organization",
379
+ description: "Short list of services / specialties (e.g. 'CGI', 'Motion Graphics', '3D Animation'). Emitted as schema.org knowsAbout — helps LLMs cite you for related queries.",
380
+ of: [{ type: "string" }],
381
+ options: { layout: "tags" },
382
+ validation: (Rule) => Rule.max(20),
383
+ }),
384
+
385
+ // === SEARCH ENGINE VERIFICATION ===
386
+ defineField({
387
+ name: "verification_google",
388
+ title: "Google Search Console",
389
+ type: "string",
390
+ group: "verification",
391
+ description: "The content value from Google Search Console's HTML tag verification method. Just the code, not the full <meta> tag.",
392
+ }),
393
+ defineField({
394
+ name: "verification_bing",
395
+ title: "Bing Webmaster",
396
+ type: "string",
397
+ group: "verification",
398
+ description: "The content value from Bing Webmaster Tools' HTML tag verification. Just the code.",
399
+ }),
400
+
299
401
  // === SETUP ===
300
402
  defineField({
301
403
  name: "setup_complete",
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @morphika/andami/site/llms-txt — AI/LLM-friendly site summary route.
3
+ *
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):
6
+ *
7
+ * // app/llms.txt/route.ts (in your instance repo)
8
+ * export { GET, revalidate } from "@morphika/andami/site/llms-txt";
9
+ */
10
+ export { GET, revalidate } from "../app/llms.txt/route";