@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.
- package/app/(site)/[slug]/page.tsx +2 -0
- package/app/(site)/layout.tsx +4 -15
- package/app/(site)/work/[slug]/page.tsx +4 -0
- package/app/admin/settings/page.tsx +136 -132
- package/app/api/admin/settings/route.ts +69 -0
- package/app/llms.txt/route.ts +142 -0
- package/app/robots.ts +73 -54
- package/app/sitemap.ts +85 -48
- package/components/admin/MetadataEditor.tsx +487 -173
- package/components/admin/SERPPreview.tsx +85 -0
- package/components/builder/settings-panel/PageSettings.tsx +79 -23
- package/components/seo/JsonLd.tsx +50 -0
- package/components/seo/ProjectJsonLd.tsx +44 -0
- package/components/seo/SiteSeoHead.tsx +66 -0
- package/lib/builder/serializer/serializers.ts +1 -0
- package/lib/sanity/queries.ts +35 -0
- package/lib/sanity/types.ts +30 -0
- package/lib/seo/jsonld.ts +174 -0
- package/lib/seo/site-settings.ts +37 -0
- package/lib/version.ts +1 -1
- package/package.json +2 -1
- package/sanity/schemas/page.ts +7 -0
- package/sanity/schemas/siteSettings.ts +102 -0
- package/site/llms-txt.ts +10 -0
package/app/robots.ts
CHANGED
|
@@ -1,54 +1,73 @@
|
|
|
1
|
-
import type { MetadataRoute } from "next";
|
|
2
|
-
import { getSiteConfig } from "../lib/config";
|
|
3
|
-
|
|
4
|
-
const cfg = getSiteConfig();
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* robots.txt — Controls crawler access and rate.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
{
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
},
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
1
|
+
import type { MetadataRoute } from "next";
|
|
2
|
+
import { getSiteConfig } from "../lib/config";
|
|
3
|
+
|
|
4
|
+
const cfg = getSiteConfig();
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* robots.txt — Controls crawler access and rate.
|
|
8
|
+
*
|
|
9
|
+
* Two-tier AI bot strategy:
|
|
10
|
+
*
|
|
11
|
+
* 1. TRAINING bots (block) — crawl pages to train LLMs on the content.
|
|
12
|
+
* Blocking them protects IP from being absorbed into model weights.
|
|
13
|
+
* Examples: GPTBot, CCBot, Google-Extended, ClaudeBot, anthropic-ai.
|
|
14
|
+
*
|
|
15
|
+
* 2. CITATION / ON-DEMAND bots (allow) — fetch URLs in real time when a
|
|
16
|
+
* user asks an AI assistant a question. Blocking them means we lose
|
|
17
|
+
* citation opportunities in ChatGPT, Perplexity, Claude responses.
|
|
18
|
+
* Examples: ChatGPT-User, Perplexity-User, Claude-User, OAI-SearchBot.
|
|
19
|
+
*
|
|
20
|
+
* Distinction matters because we want LLMs to *cite* us, not *learn from*
|
|
21
|
+
* us. The bots have different User-Agent strings; granular control via
|
|
22
|
+
* per-UA rules.
|
|
23
|
+
*
|
|
24
|
+
* Crawl-delay (seconds between requests) is honoured by Bing, Yandex,
|
|
25
|
+
* Baidu and most well-behaved bots. Googlebot ignores it but respects the
|
|
26
|
+
* rate configured in Search Console.
|
|
27
|
+
*
|
|
28
|
+
* Last reviewed: 2026-05-15.
|
|
29
|
+
*/
|
|
30
|
+
export default function robots(): MetadataRoute.Robots {
|
|
31
|
+
return {
|
|
32
|
+
rules: [
|
|
33
|
+
// ─────────────────────────────────────────────
|
|
34
|
+
// TRAINING BOTS — explicitly blocked
|
|
35
|
+
// ─────────────────────────────────────────────
|
|
36
|
+
{ userAgent: "GPTBot", disallow: ["/"] }, // OpenAI training crawler
|
|
37
|
+
{ userAgent: "CCBot", disallow: ["/"] }, // Common Crawl (feeds most LLMs)
|
|
38
|
+
{ userAgent: "Google-Extended", disallow: ["/"] }, // Google's AI training (separate from Googlebot)
|
|
39
|
+
{ userAgent: "anthropic-ai", disallow: ["/"] }, // Older Anthropic training UA
|
|
40
|
+
{ userAgent: "ClaudeBot", disallow: ["/"] }, // Anthropic Claude training
|
|
41
|
+
{ userAgent: "FacebookBot", disallow: ["/"] }, // Meta/LLaMA training
|
|
42
|
+
{ userAgent: "Applebot-Extended", disallow: ["/"] }, // Apple Intelligence training (separate from Applebot which indexes for Siri/Spotlight — that one is allowed implicitly)
|
|
43
|
+
{ userAgent: "Bytespider", disallow: ["/"] }, // ByteDance / TikTok training crawler
|
|
44
|
+
{ userAgent: "PetalBot", disallow: ["/"] }, // Huawei / Petal Search aggressive crawler
|
|
45
|
+
|
|
46
|
+
// ─────────────────────────────────────────────
|
|
47
|
+
// CITATION / ON-DEMAND BOTS — explicitly allowed
|
|
48
|
+
// (they're allowed by default anyway, but we list
|
|
49
|
+
// them to make the policy explicit and to override
|
|
50
|
+
// any future changes to the catch-all rule.)
|
|
51
|
+
// ─────────────────────────────────────────────
|
|
52
|
+
{ userAgent: "ChatGPT-User", allow: "/" }, // ChatGPT user-triggered fetches
|
|
53
|
+
{ userAgent: "OAI-SearchBot", allow: "/" }, // OpenAI search index
|
|
54
|
+
{ userAgent: "PerplexityBot", allow: "/" }, // Perplexity index
|
|
55
|
+
{ userAgent: "Perplexity-User", allow: "/" }, // Perplexity user-triggered fetches
|
|
56
|
+
{ userAgent: "Claude-User", allow: "/" }, // Claude user-triggered fetches
|
|
57
|
+
{ userAgent: "Claude-Web", allow: "/" }, // Claude web search
|
|
58
|
+
{ userAgent: "Google-CloudVertexBot", allow: "/" }, // Vertex AI on-demand fetch
|
|
59
|
+
{ userAgent: "YouBot", allow: "/" }, // You.com AI search
|
|
60
|
+
|
|
61
|
+
// ─────────────────────────────────────────────
|
|
62
|
+
// DEFAULT — Googlebot, Bingbot, and everyone else
|
|
63
|
+
// ─────────────────────────────────────────────
|
|
64
|
+
{
|
|
65
|
+
userAgent: "*",
|
|
66
|
+
allow: "/",
|
|
67
|
+
disallow: ["/admin/", "/studio/", "/api/admin/", "/api/"],
|
|
68
|
+
crawlDelay: 10,
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
sitemap: `${cfg.domain}/sitemap.xml`,
|
|
72
|
+
};
|
|
73
|
+
}
|
package/app/sitemap.ts
CHANGED
|
@@ -1,48 +1,85 @@
|
|
|
1
|
-
import type { MetadataRoute } from "next";
|
|
2
|
-
import { client } from "../lib/sanity/client";
|
|
3
|
-
import {
|
|
4
|
-
import { getSiteConfig } from "../lib/config";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
1
|
+
import type { MetadataRoute } from "next";
|
|
2
|
+
import { client } from "../lib/sanity/client";
|
|
3
|
+
import { allPagesForSitemapQuery, allProjectsForSitemapQuery } from "../lib/sanity/queries";
|
|
4
|
+
import { getSiteConfig } from "../lib/config";
|
|
5
|
+
import { assetUrl } from "../lib/assets";
|
|
6
|
+
import { toAbsoluteUrl } from "../lib/seo/site-settings";
|
|
7
|
+
|
|
8
|
+
const cfg = getSiteConfig();
|
|
9
|
+
|
|
10
|
+
interface PageSitemapEntry {
|
|
11
|
+
slug: string;
|
|
12
|
+
updatedAt: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ProjectSitemapEntry extends PageSitemapEntry {
|
|
16
|
+
thumbnail_path?: string;
|
|
17
|
+
title?: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
published_at?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* sitemap.xml — Dynamic generation from Sanity content.
|
|
24
|
+
*
|
|
25
|
+
* Each entry's <lastmod> uses Sanity's _updatedAt field (not the build time),
|
|
26
|
+
* which is the correct SEO signal: Google deprioritizes sitemaps with all
|
|
27
|
+
* identical timestamps.
|
|
28
|
+
*
|
|
29
|
+
* Project entries include their thumbnail in the `images` array — Next.js
|
|
30
|
+
* emits this as <image:image> nodes per the Google Image Sitemap protocol,
|
|
31
|
+
* allowing Google Images to index project thumbnails alongside the page URL.
|
|
32
|
+
*
|
|
33
|
+
* Pages/projects with metadata.noindex == true are excluded at the GROQ
|
|
34
|
+
* level (see lib/sanity/queries.ts).
|
|
35
|
+
*/
|
|
36
|
+
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|
37
|
+
const baseUrl = cfg.domain;
|
|
38
|
+
|
|
39
|
+
const [pages, projects] = await Promise.all([
|
|
40
|
+
client.fetch<PageSitemapEntry[]>(allPagesForSitemapQuery).catch(() => [] as PageSitemapEntry[]),
|
|
41
|
+
client.fetch<ProjectSitemapEntry[]>(allProjectsForSitemapQuery).catch(() => [] as ProjectSitemapEntry[]),
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
// Homepage — lastModified derived from the most recent page or project update
|
|
45
|
+
const allTimestamps = [
|
|
46
|
+
...pages.map((p) => p.updatedAt),
|
|
47
|
+
...projects.map((p) => p.updatedAt),
|
|
48
|
+
].filter(Boolean);
|
|
49
|
+
const homeLastMod = allTimestamps.length > 0
|
|
50
|
+
? new Date(Math.max(...allTimestamps.map((t) => new Date(t).getTime())))
|
|
51
|
+
: new Date();
|
|
52
|
+
|
|
53
|
+
const routes: MetadataRoute.Sitemap = [
|
|
54
|
+
{
|
|
55
|
+
url: baseUrl,
|
|
56
|
+
lastModified: homeLastMod,
|
|
57
|
+
changeFrequency: "weekly",
|
|
58
|
+
priority: 1,
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
for (const { slug, updatedAt } of pages) {
|
|
63
|
+
routes.push({
|
|
64
|
+
url: `${baseUrl}/${slug}`,
|
|
65
|
+
lastModified: updatedAt ? new Date(updatedAt) : new Date(),
|
|
66
|
+
changeFrequency: "monthly",
|
|
67
|
+
priority: 0.8,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (const { slug, updatedAt, thumbnail_path } of projects) {
|
|
72
|
+
const thumbnailUrl = thumbnail_path
|
|
73
|
+
? toAbsoluteUrl(assetUrl(thumbnail_path), baseUrl)
|
|
74
|
+
: undefined;
|
|
75
|
+
routes.push({
|
|
76
|
+
url: `${baseUrl}/work/${slug}`,
|
|
77
|
+
lastModified: updatedAt ? new Date(updatedAt) : new Date(),
|
|
78
|
+
changeFrequency: "monthly",
|
|
79
|
+
priority: 0.7,
|
|
80
|
+
...(thumbnailUrl && { images: [thumbnailUrl] }),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return routes;
|
|
85
|
+
}
|