@morphika/andami 0.6.0 → 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,50 @@
1
+ /**
2
+ * JsonLd — Inject one or more JSON-LD structured-data objects as
3
+ * <script type="application/ld+json"> tags.
4
+ *
5
+ * Accepts either a single object or an array. Null/undefined entries are
6
+ * filtered out, so callers can pass build results directly:
7
+ *
8
+ * <JsonLd data={[
9
+ * buildOrganizationJsonLd({ ... }),
10
+ * buildWebSiteJsonLd({ ... }),
11
+ * ]} />
12
+ *
13
+ * Server-rendered safely — uses `dangerouslySetInnerHTML` with a
14
+ * JSON.stringify that guards against XSS via `</script>` injection.
15
+ */
16
+
17
+ type LdRecord = Record<string, unknown>;
18
+
19
+ interface JsonLdProps {
20
+ data: LdRecord | null | undefined | Array<LdRecord | null | undefined>;
21
+ }
22
+
23
+ /**
24
+ * Escape `</` sequences inside JSON to prevent breaking out of the
25
+ * <script> tag. Standard hardening per OWASP recommendation.
26
+ */
27
+ function safeStringify(obj: LdRecord): string {
28
+ return JSON.stringify(obj).replace(/<\/(script)/gi, "<\\/$1");
29
+ }
30
+
31
+ export default function JsonLd({ data }: JsonLdProps) {
32
+ const items = (Array.isArray(data) ? data : [data]).filter(
33
+ (item): item is LdRecord => item != null && typeof item === "object"
34
+ );
35
+
36
+ if (items.length === 0) return null;
37
+
38
+ return (
39
+ <>
40
+ {items.map((item, i) => (
41
+ <script
42
+ key={i}
43
+ type="application/ld+json"
44
+ // eslint-disable-next-line react/no-danger
45
+ dangerouslySetInnerHTML={{ __html: safeStringify(item) }}
46
+ />
47
+ ))}
48
+ </>
49
+ );
50
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * ProjectJsonLd — Server component that emits a `CreativeWork` JSON-LD
3
+ * record for a project page (`/work/[slug]`). Rendered inline at the top
4
+ * of the project page so search engines see the structured data alongside
5
+ * the HTML body.
6
+ *
7
+ * Falls back to siteSettings.default_author for the author field when the
8
+ * page doesn't specify one. Returns null when the page has insufficient
9
+ * data — search engines tolerate partial schemas, but emitting an empty
10
+ * CreativeWork would be noise.
11
+ */
12
+
13
+ import JsonLd from "./JsonLd";
14
+ import { buildCreativeWorkJsonLd } from "../../lib/seo/jsonld";
15
+ import { getCachedSiteSettings, toAbsoluteUrl } from "../../lib/seo/site-settings";
16
+ import { getSiteConfig } from "../../lib/config";
17
+ import { assetUrl } from "../../lib/assets";
18
+ import type { Page } from "../../lib/sanity/types";
19
+
20
+ interface ProjectJsonLdProps {
21
+ page: Page;
22
+ }
23
+
24
+ export default async function ProjectJsonLd({ page }: ProjectJsonLdProps) {
25
+ const settings = await getCachedSiteSettings();
26
+ const cfg = getSiteConfig();
27
+
28
+ const url = `${cfg.domain.replace(/\/$/, "")}/work/${page.slug?.current || ""}`;
29
+ const imageAbsolute = page.thumbnail_path
30
+ ? toAbsoluteUrl(assetUrl(page.thumbnail_path), cfg.domain)
31
+ : undefined;
32
+ const authorName = settings?.default_author || cfg.name;
33
+
34
+ const ld = buildCreativeWorkJsonLd({
35
+ name: page.title,
36
+ url,
37
+ image: imageAbsolute,
38
+ datePublished: page.published_at,
39
+ description: page.metadata?.seo_description,
40
+ authorName,
41
+ });
42
+
43
+ return <JsonLd data={ld} />;
44
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * SiteSeoHead — Server component that emits site-wide SEO surfaces:
3
+ *
4
+ * - JSON-LD `Organization` (logo + sameAs profiles → Google Knowledge Panel)
5
+ * - JSON-LD `WebSite` (basic site identity)
6
+ * - `<meta name="google-site-verification">` (when configured)
7
+ * - `<meta name="msvalidate.01">` for Bing Webmaster (when configured)
8
+ *
9
+ * Rendered inside the public site layout so every public page emits these.
10
+ * Admin and Studio routes are excluded (different layout subtree).
11
+ *
12
+ * Data source: siteSettings document in Sanity (cached via React `cache()`).
13
+ * Empty fields are omitted — partial schemas are tolerated by search engines.
14
+ */
15
+
16
+ import JsonLd from "./JsonLd";
17
+ import { buildOrganizationJsonLd, buildWebSiteJsonLd } from "../../lib/seo/jsonld";
18
+ import { getCachedSiteSettings, toAbsoluteUrl } from "../../lib/seo/site-settings";
19
+ import { getSiteConfig } from "../../lib/config";
20
+ import { assetUrl } from "../../lib/assets";
21
+
22
+ export default async function SiteSeoHead() {
23
+ const settings = await getCachedSiteSettings();
24
+ const cfg = getSiteConfig();
25
+
26
+ const logoAbsolute = settings?.org_logo
27
+ ? toAbsoluteUrl(assetUrl(settings.org_logo), cfg.domain)
28
+ : undefined;
29
+
30
+ const description =
31
+ settings?.default_description || cfg.defaults.metaDescription || undefined;
32
+
33
+ const orgLd = buildOrganizationJsonLd({
34
+ name: cfg.name,
35
+ url: cfg.domain,
36
+ logo: logoAbsolute,
37
+ socialLinks: settings?.social_links,
38
+ description,
39
+ foundingYear: settings?.founding_year,
40
+ addressLocality: settings?.address_locality,
41
+ addressCountry: settings?.address_country,
42
+ contactEmail: settings?.contact_email,
43
+ keywords: settings?.keywords,
44
+ });
45
+
46
+ const siteLd = buildWebSiteJsonLd({
47
+ name: cfg.name,
48
+ url: cfg.domain,
49
+ description,
50
+ });
51
+
52
+ return (
53
+ <>
54
+ <JsonLd data={[orgLd, siteLd]} />
55
+ {settings?.verification_google && (
56
+ <meta
57
+ name="google-site-verification"
58
+ content={settings.verification_google}
59
+ />
60
+ )}
61
+ {settings?.verification_bing && (
62
+ <meta name="msvalidate.01" content={settings.verification_bing} />
63
+ )}
64
+ </>
65
+ );
66
+ }
@@ -227,12 +227,15 @@ export const allPageSlugsQuery = groq`
227
227
  *[_type == "page" && page_type != "project" && draft_mode != true && defined(slug.current)].slug.current
228
228
  `;
229
229
 
230
- // All non-project pages for sitemap.xml — includes _updatedAt for accurate <lastmod>
231
- // and respects metadata.noindex to exclude hidden pages from the sitemap.
230
+ // All non-project pages for sitemap.xml + llms.txt — includes _updatedAt for
231
+ // accurate <lastmod>, plus title and description for llms.txt entries.
232
+ // Respects metadata.noindex to exclude hidden pages.
232
233
  export const allPagesForSitemapQuery = groq`
233
- *[_type == "page" && page_type != "project" && draft_mode != true && defined(slug.current) && metadata.noindex != true] {
234
+ *[_type == "page" && page_type != "project" && draft_mode != true && defined(slug.current) && metadata.noindex != true] | order(is_home desc, title asc) {
234
235
  "slug": slug.current,
235
- "updatedAt": _updatedAt
236
+ "updatedAt": _updatedAt,
237
+ title,
238
+ "description": metadata.seo_description
236
239
  }
237
240
  `;
238
241
 
@@ -258,12 +261,16 @@ export const allProjectSlugsQuery = groq`
258
261
  *[_type == "page" && page_type == "project" && draft_mode != true && defined(slug.current)].slug.current
259
262
  `;
260
263
 
261
- // All published projects for sitemap.xml — includes _updatedAt for accurate <lastmod>
262
- // and respects metadata.noindex to exclude hidden projects from the sitemap.
264
+ // All published projects for sitemap.xml — includes _updatedAt for accurate <lastmod>,
265
+ // thumbnail_path for image sitemap, and respects metadata.noindex.
263
266
  export const allProjectsForSitemapQuery = groq`
264
267
  *[_type == "page" && page_type == "project" && draft_mode != true && defined(slug.current) && metadata.noindex != true] {
265
268
  "slug": slug.current,
266
- "updatedAt": _updatedAt
269
+ "updatedAt": _updatedAt,
270
+ thumbnail_path,
271
+ title,
272
+ "description": metadata.seo_description,
273
+ published_at
267
274
  }
268
275
  `;
269
276
 
@@ -440,6 +447,16 @@ export const siteSettingsQuery = groq`
440
447
  default_og_image,
441
448
  favicon_path,
442
449
  analytics_id,
450
+ org_logo,
451
+ default_author,
452
+ social_links,
453
+ founding_year,
454
+ address_locality,
455
+ address_country,
456
+ contact_email,
457
+ keywords,
458
+ verification_google,
459
+ verification_bing,
443
460
  seed_url,
444
461
  last_scanned_at,
445
462
  asset_status
@@ -1031,6 +1031,14 @@ export interface AssetRegistry {
1031
1031
  // Site Settings
1032
1032
  // ============================================
1033
1033
 
1034
+ export interface SocialLink {
1035
+ _key?: string;
1036
+ /** Optional display label (e.g. "Instagram", "Behance"). For admin organization only. */
1037
+ label?: string;
1038
+ /** Profile URL — emitted as schema.org sameAs[]. */
1039
+ url: string;
1040
+ }
1041
+
1034
1042
  export interface SiteSettings {
1035
1043
  nav_items?: NavItem[];
1036
1044
  nav_design?: NavDesign;
@@ -1040,6 +1048,26 @@ export interface SiteSettings {
1040
1048
  default_og_image?: string;
1041
1049
  favicon_path?: string;
1042
1050
  analytics_id?: string;
1051
+ /** Asset path to the organization logo used by JSON-LD `Organization.logo`. */
1052
+ org_logo?: string;
1053
+ /** Default author for project pages (used in `CreativeWork.author` when not overridden). */
1054
+ default_author?: string;
1055
+ /** Social profile URLs emitted as schema.org `sameAs[]`. */
1056
+ social_links?: SocialLink[];
1057
+ /** Year the organization was founded. Emitted as schema.org `foundingDate`. */
1058
+ founding_year?: number;
1059
+ /** City/town (schema.org `address.addressLocality`). */
1060
+ address_locality?: string;
1061
+ /** Country name or ISO code (schema.org `address.addressCountry`). */
1062
+ address_country?: string;
1063
+ /** Public contact email (schema.org `contactPoint.email`). */
1064
+ contact_email?: string;
1065
+ /** Areas of expertise — short tags emitted as schema.org `knowsAbout[]`. */
1066
+ keywords?: string[];
1067
+ /** Google Search Console verification code (HTML tag method). */
1068
+ verification_google?: string;
1069
+ /** Bing Webmaster Tools verification code. */
1070
+ verification_bing?: string;
1043
1071
  seed_url?: string;
1044
1072
  last_scanned_at?: string;
1045
1073
  asset_status?: "ok" | "scanning" | "error";
@@ -0,0 +1,174 @@
1
+ /**
2
+ * JSON-LD structured data builders for schema.org types.
3
+ *
4
+ * Pure functions — each takes input data and returns a plain JSON-LD object
5
+ * ready to be serialized into a <script type="application/ld+json"> tag.
6
+ *
7
+ * Empty/missing fields are simply omitted from the output (search engines
8
+ * tolerate partial schemas). All builders return `null` when the minimum
9
+ * required fields aren't present, so callers can skip emission entirely.
10
+ *
11
+ * Schema reference: https://schema.org/
12
+ * Google guidance: https://developers.google.com/search/docs/appearance/structured-data
13
+ */
14
+
15
+ import type { SocialLink } from "../sanity/types";
16
+
17
+ // ============================================
18
+ // Types
19
+ // ============================================
20
+
21
+ export interface OrganizationInput {
22
+ name: string;
23
+ url: string;
24
+ /** Absolute URL to the organization logo (NOT a relative asset path). */
25
+ logo?: string;
26
+ /** Profile URLs (filtered & trimmed). */
27
+ socialLinks?: SocialLink[];
28
+ /** Optional descriptive tagline for the organization. */
29
+ description?: string;
30
+ /** Year the org was founded (emitted as ISO date `YYYY`). */
31
+ foundingYear?: number;
32
+ /** City/town of the org's primary address. */
33
+ addressLocality?: string;
34
+ /** Country name or ISO code. */
35
+ addressCountry?: string;
36
+ /** Public contact email (creates a `contactPoint`). */
37
+ contactEmail?: string;
38
+ /** Tags / areas of expertise emitted as `knowsAbout[]`. */
39
+ keywords?: string[];
40
+ }
41
+
42
+ export interface WebSiteInput {
43
+ name: string;
44
+ url: string;
45
+ description?: string;
46
+ }
47
+
48
+ export interface CreativeWorkInput {
49
+ /** Title of the work. */
50
+ name: string;
51
+ /** Absolute URL of the work's page. */
52
+ url: string;
53
+ /** Absolute URL to the work's primary image (thumbnail / hero). */
54
+ image?: string;
55
+ /** ISO 8601 publication date. */
56
+ datePublished?: string;
57
+ /** Short descriptive text. */
58
+ description?: string;
59
+ /** Name of the creating organization or person (falls back to org name). */
60
+ authorName?: string;
61
+ }
62
+
63
+ // ============================================
64
+ // Builders
65
+ // ============================================
66
+
67
+ /**
68
+ * Organization — emitted site-wide. Powers Google Knowledge Panel for brand
69
+ * searches when combined with verified profiles via `sameAs`.
70
+ */
71
+ export function buildOrganizationJsonLd(
72
+ input: OrganizationInput
73
+ ): Record<string, unknown> | null {
74
+ if (!input.name || !input.url) return null;
75
+
76
+ const ld: Record<string, unknown> = {
77
+ "@context": "https://schema.org",
78
+ "@type": "Organization",
79
+ name: input.name,
80
+ url: input.url,
81
+ };
82
+
83
+ if (input.logo) ld.logo = input.logo;
84
+ if (input.description) ld.description = input.description;
85
+
86
+ // foundingDate — schema.org expects ISO date; year-only is valid per spec
87
+ if (input.foundingYear && Number.isFinite(input.foundingYear)) {
88
+ ld.foundingDate = String(Math.round(input.foundingYear));
89
+ }
90
+
91
+ // Address — PostalAddress with locality/country
92
+ if (input.addressLocality || input.addressCountry) {
93
+ const address: Record<string, unknown> = {
94
+ "@type": "PostalAddress",
95
+ };
96
+ if (input.addressLocality) address.addressLocality = input.addressLocality;
97
+ if (input.addressCountry) address.addressCountry = input.addressCountry;
98
+ ld.address = address;
99
+ }
100
+
101
+ // ContactPoint — only emit if email present (schema.org requires contactType)
102
+ if (input.contactEmail) {
103
+ ld.contactPoint = {
104
+ "@type": "ContactPoint",
105
+ contactType: "customer service",
106
+ email: input.contactEmail,
107
+ };
108
+ }
109
+
110
+ // knowsAbout — array of strings (or schema.org Things)
111
+ const knowsAbout = (input.keywords || [])
112
+ .map((k) => k?.trim())
113
+ .filter((k): k is string => Boolean(k));
114
+ if (knowsAbout.length > 0) ld.knowsAbout = knowsAbout;
115
+
116
+ // sameAs — social profile URLs
117
+ const sameAs = (input.socialLinks || [])
118
+ .map((l) => l?.url?.trim())
119
+ .filter((u): u is string => Boolean(u));
120
+ if (sameAs.length > 0) ld.sameAs = sameAs;
121
+
122
+ return ld;
123
+ }
124
+
125
+ /**
126
+ * WebSite — emitted site-wide. Helps Google identify the site as a coherent
127
+ * web property (separate from individual pages).
128
+ */
129
+ export function buildWebSiteJsonLd(
130
+ input: WebSiteInput
131
+ ): Record<string, unknown> | null {
132
+ if (!input.name || !input.url) return null;
133
+
134
+ const ld: Record<string, unknown> = {
135
+ "@context": "https://schema.org",
136
+ "@type": "WebSite",
137
+ name: input.name,
138
+ url: input.url,
139
+ };
140
+
141
+ if (input.description) ld.description = input.description;
142
+
143
+ return ld;
144
+ }
145
+
146
+ /**
147
+ * CreativeWork — emitted on project pages. Tells Google the page represents a
148
+ * specific creative work (motion graphic, illustration, photograph, etc.) and
149
+ * unlocks richer result presentation.
150
+ */
151
+ export function buildCreativeWorkJsonLd(
152
+ input: CreativeWorkInput
153
+ ): Record<string, unknown> | null {
154
+ if (!input.name || !input.url) return null;
155
+
156
+ const ld: Record<string, unknown> = {
157
+ "@context": "https://schema.org",
158
+ "@type": "CreativeWork",
159
+ name: input.name,
160
+ url: input.url,
161
+ };
162
+
163
+ if (input.image) ld.image = input.image;
164
+ if (input.datePublished) ld.datePublished = input.datePublished;
165
+ if (input.description) ld.description = input.description;
166
+ if (input.authorName) {
167
+ ld.author = {
168
+ "@type": "Organization",
169
+ name: input.authorName,
170
+ };
171
+ }
172
+
173
+ return ld;
174
+ }
@@ -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.6.0";
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.6.0",
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",
@@ -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";