bsmnt 0.2.7 → 0.2.9
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/README.md +1 -1
- package/dist/helpers/create/index.d.ts +2 -1
- package/dist/helpers/create/index.d.ts.map +1 -1
- package/dist/helpers/create/index.js +16 -5
- package/dist/helpers/create/index.js.map +1 -1
- package/dist/helpers/create/init-git.d.ts +12 -0
- package/dist/helpers/create/init-git.d.ts.map +1 -0
- package/dist/helpers/create/init-git.js +34 -0
- package/dist/helpers/create/init-git.js.map +1 -0
- package/dist/helpers/create/setup-agent.d.ts +3 -2
- package/dist/helpers/create/setup-agent.d.ts.map +1 -1
- package/dist/helpers/create/setup-agent.js +42 -31
- package/dist/helpers/create/setup-agent.js.map +1 -1
- package/dist/helpers/create/setup-sanity.d.ts.map +1 -1
- package/dist/helpers/create/setup-sanity.js +31 -25
- package/dist/helpers/create/setup-sanity.js.map +1 -1
- package/dist/helpers/create/update-package.d.ts.map +1 -1
- package/dist/helpers/create/update-package.js +10 -0
- package/dist/helpers/create/update-package.js.map +1 -1
- package/dist/helpers/integrate/index.d.ts.map +1 -1
- package/dist/helpers/integrate/index.js +27 -13
- package/dist/helpers/integrate/index.js.map +1 -1
- package/dist/helpers/integrate/merge-config.d.ts.map +1 -1
- package/dist/helpers/integrate/merge-config.js +5 -0
- package/dist/helpers/integrate/merge-config.js.map +1 -1
- package/dist/helpers/integrate/merge-orchestrator.js +1 -1
- package/dist/helpers/integrate/merge-orchestrator.js.map +1 -1
- package/dist/helpers/integrate/sanity/config.d.ts.map +1 -1
- package/dist/helpers/integrate/sanity/config.js +12 -1
- package/dist/helpers/integrate/sanity/config.js.map +1 -1
- package/dist/helpers/integrate/sanity/mergers/sitemap-merger.d.ts.map +1 -1
- package/dist/helpers/integrate/sanity/mergers/sitemap-merger.js +2 -21
- package/dist/helpers/integrate/sanity/mergers/sitemap-merger.js.map +1 -1
- package/dist/index.js +48 -19
- package/dist/index.js.map +1 -1
- package/index.js +4 -3
- package/package.json +7 -2
- package/src/helpers/integrate/sanity/files/app/api/blog/[slug]/route.ts +75 -0
- package/src/helpers/integrate/sanity/files/app/blog/[slug]/page.tsx +56 -0
- package/src/helpers/integrate/sanity/files/app/sitemap.md/route.ts +82 -0
- package/src/helpers/integrate/sanity/files/app/sitemap.ts +2 -21
- package/src/helpers/integrate/sanity/files/lib/integrations/README.md +1 -1
- package/src/helpers/integrate/sanity/files/lib/integrations/sanity/MARKDOWN-PROXY.md +273 -0
- package/src/helpers/integrate/sanity/files/lib/integrations/sanity/README.md +9 -9
- package/src/helpers/integrate/sanity/files/lib/integrations/sanity/components/rich-text.tsx +2 -2
- package/src/helpers/integrate/sanity/files/lib/integrations/sanity/live/index.tsx +53 -8
- package/src/helpers/integrate/sanity/files/lib/integrations/sanity/markdown-proxy.config.ts +49 -0
- package/src/helpers/integrate/sanity/files/lib/integrations/sanity/queries.ts +7 -40
- package/src/helpers/integrate/sanity/files/lib/integrations/sanity/sanity.cli.ts +5 -0
- package/src/helpers/integrate/sanity/files/lib/integrations/sanity/sanity.config.ts +2 -22
- package/src/helpers/integrate/sanity/files/lib/integrations/sanity/sanity.types.ts +1 -24
- package/src/helpers/integrate/sanity/files/lib/integrations/sanity/schema.json +2 -124
- package/src/helpers/integrate/sanity/files/lib/integrations/sanity/schemas/index.ts +1 -3
- package/src/helpers/integrate/sanity/files/lib/integrations/sanity/schemas/link.ts +2 -2
- package/src/helpers/integrate/sanity/files/lib/integrations/sanity/utils/link.ts +25 -2
- package/src/helpers/integrate/sanity/files/lib/scripts/generate-page.ts +17 -30
- package/src/helpers/integrate/sanity/files/lib/utils/metadata.ts +10 -10
- package/src/helpers/integrate/sanity/files/lib/utils/portable-text-to-markdown.ts +24 -0
- package/src/helpers/integrate/sanity/files/proxy.ts +66 -0
- package/src/templates/next-default/components/ui/image/image.module.css +5 -0
- package/src/templates/next-default/components/ui/image/index.tsx +1 -2
- package/src/templates/next-default/lib/scripts/generate.ts +2 -2
- package/src/templates/next-default/lib/utils/metadata.ts +1 -1
- package/src/templates/next-default/next.config.ts +0 -1
- package/src/templates/next-experiments/components/ui/image/image.module.css +5 -0
- package/src/templates/next-experiments/components/ui/image/index.tsx +3 -2
- package/src/templates/next-experiments/lib/scripts/generate.ts +2 -2
- package/src/templates/next-experiments/lib/utils/metadata.ts +1 -1
- package/src/templates/next-experiments/next.config.ts +0 -1
- package/src/templates/next-webgl/components/ui/image/image.module.css +5 -0
- package/src/templates/next-webgl/components/ui/image/index.tsx +1 -2
- package/src/templates/next-webgl/lib/scripts/generate.ts +2 -2
- package/src/templates/next-webgl/lib/utils/metadata.ts +1 -1
- package/src/templates/next-webgl/next.config.ts +0 -1
- package/src/helpers/integrate/sanity/files/lib/integrations/sanity/schemas/page.ts +0 -77
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { isSanityConfigured } from "@/lib/integrations/check-integration";
|
|
3
|
+
|
|
4
|
+
export async function GET() {
|
|
5
|
+
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000";
|
|
6
|
+
|
|
7
|
+
if (!isSanityConfigured()) {
|
|
8
|
+
return new NextResponse(
|
|
9
|
+
"# Sitemap\n\nCMS not configured. No content available.",
|
|
10
|
+
{
|
|
11
|
+
headers: { "Content-Type": "text/markdown; charset=utf-8" },
|
|
12
|
+
},
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const sanityModule = await import("@/lib/integrations/sanity/client");
|
|
18
|
+
const sanityGroq = await import("next-sanity");
|
|
19
|
+
|
|
20
|
+
const client = sanityModule?.client;
|
|
21
|
+
const groq = sanityGroq?.groq;
|
|
22
|
+
|
|
23
|
+
if (!(client && groq)) {
|
|
24
|
+
return new NextResponse("# Sitemap\n\nUnable to connect to CMS.", {
|
|
25
|
+
status: 503,
|
|
26
|
+
headers: { "Content-Type": "text/markdown; charset=utf-8" },
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type SanityDoc = {
|
|
31
|
+
title: string;
|
|
32
|
+
slug: { current: string };
|
|
33
|
+
_updatedAt: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const articles = await client.fetch<SanityDoc[]>(
|
|
37
|
+
groq`*[_type == "article" && defined(slug.current)] | order(publishedAt desc) {
|
|
38
|
+
title,
|
|
39
|
+
slug,
|
|
40
|
+
_updatedAt
|
|
41
|
+
}`,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const parts = [
|
|
45
|
+
`# ${new URL(baseUrl).hostname} - Content Sitemap`,
|
|
46
|
+
"",
|
|
47
|
+
"> This sitemap lists all content available in markdown format for AI agents.",
|
|
48
|
+
"> Access any link below directly, or request page URLs with `Accept: text/markdown` header.",
|
|
49
|
+
"",
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
if (articles.length > 0) {
|
|
53
|
+
parts.push("## Articles", "");
|
|
54
|
+
for (const article of articles) {
|
|
55
|
+
parts.push(
|
|
56
|
+
`- [${article.title}](${baseUrl}/blog/${article.slug.current}.md)`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
parts.push("");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (articles.length === 0) {
|
|
63
|
+
parts.push("No content published yet.", "");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const markdown = parts.join("\n");
|
|
67
|
+
|
|
68
|
+
return new NextResponse(markdown, {
|
|
69
|
+
headers: {
|
|
70
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
71
|
+
"X-Content-Type-Options": "nosniff",
|
|
72
|
+
"Cache-Control": "public, s-maxage=3600, stale-while-revalidate=600",
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error("Error generating sitemap.md:", error);
|
|
77
|
+
return new NextResponse("# Sitemap\n\nError fetching content.", {
|
|
78
|
+
status: 500,
|
|
79
|
+
headers: { "Content-Type": "text/markdown; charset=utf-8" },
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -14,7 +14,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|
|
14
14
|
},
|
|
15
15
|
];
|
|
16
16
|
|
|
17
|
-
// Only fetch Sanity
|
|
17
|
+
// Only fetch Sanity articles if Sanity is configured
|
|
18
18
|
if (isSanityConfigured()) {
|
|
19
19
|
try {
|
|
20
20
|
const sanityModule = await import("@/lib/integrations/sanity/client");
|
|
@@ -32,15 +32,6 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|
|
32
32
|
metadata?: { noIndex?: boolean };
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
-
// Fetch all published pages and articles
|
|
36
|
-
const pages = (await client.fetch(
|
|
37
|
-
groq`*[_type == "page" && defined(slug.current)] {
|
|
38
|
-
slug,
|
|
39
|
-
_updatedAt,
|
|
40
|
-
metadata
|
|
41
|
-
}`,
|
|
42
|
-
)) as SanityDocument[];
|
|
43
|
-
|
|
44
35
|
const articles = (await client.fetch(
|
|
45
36
|
groq`*[_type == "article" && defined(slug.current)] {
|
|
46
37
|
slug,
|
|
@@ -49,16 +40,6 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|
|
49
40
|
}`,
|
|
50
41
|
)) as SanityDocument[];
|
|
51
42
|
|
|
52
|
-
// Add pages to sitemap (exclude noIndex pages)
|
|
53
|
-
const pageEntries: MetadataRoute.Sitemap = pages
|
|
54
|
-
.filter((page: SanityDocument) => !page.metadata?.noIndex)
|
|
55
|
-
.map((page: SanityDocument) => ({
|
|
56
|
-
url: `${APP_BASE_URL}/${page.slug.current}`,
|
|
57
|
-
lastModified: new Date(page._updatedAt),
|
|
58
|
-
changeFrequency: "weekly" as const,
|
|
59
|
-
priority: 0.8,
|
|
60
|
-
}));
|
|
61
|
-
|
|
62
43
|
// Add articles to sitemap (exclude noIndex articles)
|
|
63
44
|
const articleEntries: MetadataRoute.Sitemap = articles
|
|
64
45
|
.filter((article: SanityDocument) => !article.metadata?.noIndex)
|
|
@@ -69,7 +50,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|
|
69
50
|
priority: 0.7,
|
|
70
51
|
}));
|
|
71
52
|
|
|
72
|
-
return [...baseRoutes, ...
|
|
53
|
+
return [...baseRoutes, ...articleEntries];
|
|
73
54
|
} catch (error) {
|
|
74
55
|
console.error("Error generating sitemap from Sanity:", error);
|
|
75
56
|
return baseRoutes;
|
|
@@ -34,7 +34,7 @@ NEXT_PUBLIC_GOOGLE_ANALYTICS=G-XXXXXXXXXX
|
|
|
34
34
|
// Sanity
|
|
35
35
|
import { sanityFetch } from '@/integrations/sanity/live'
|
|
36
36
|
import { RichText } from '@/integrations/sanity/components/rich-text'
|
|
37
|
-
const { data } = await sanityFetch({ query:
|
|
37
|
+
const { data } = await sanityFetch({ query: ARTICLE_QUERY })
|
|
38
38
|
|
|
39
39
|
// Shopify
|
|
40
40
|
import { Cart, AddToCart } from '@/lib/integrations/shopify/cart'
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# Markdown Proxy — Adding New Content Types
|
|
2
|
+
|
|
3
|
+
This project serves Sanity content as markdown for AI agents. Each content type (article, etc.) has a dedicated route handler that fetches from Sanity and returns formatted markdown.
|
|
4
|
+
|
|
5
|
+
## Architecture Overview
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Request: GET /blog/my-post.md
|
|
9
|
+
|
|
|
10
|
+
proxy.ts — rewrites to /api/blog/my-post.md
|
|
11
|
+
|
|
|
12
|
+
app/api/blog/[slug]/route.ts — validates .md extension, strips it, fetches from Sanity, returns markdown
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Route handlers live under `/api/` so `page.tsx` can coexist at the public path (e.g. `app/blog/[slug]/page.tsx` for the HTML page). The `proxy.ts` file rewrites `.md` URLs to the API routes transparently.
|
|
16
|
+
|
|
17
|
+
**Key files:**
|
|
18
|
+
|
|
19
|
+
| File | Purpose |
|
|
20
|
+
|------|---------|
|
|
21
|
+
| `app/api/{type}/[slug]/route.ts` | API route handler per content type (serves markdown) |
|
|
22
|
+
| `app/sitemap.md/route.ts` | Discovery endpoint listing all markdown content |
|
|
23
|
+
| `proxy.ts` | Rewrites `.md` URLs to API routes, handles `Accept: text/markdown` |
|
|
24
|
+
| `lib/integrations/sanity/markdown-proxy.config.ts` | Route config for proxy rewrites |
|
|
25
|
+
| `lib/integrations/sanity/utils/link.ts` | `CONTENT_TYPE_TO_PATH` mapping for internal link resolution |
|
|
26
|
+
| `lib/integrations/sanity/queries.ts` | GROQ queries for each content type |
|
|
27
|
+
| `lib/utils/portable-text-to-markdown.ts` | Shared Portable Text to markdown converter |
|
|
28
|
+
| `lib/integrations/sanity/live/index.tsx` | `SanityFetch` helper for markdown routes |
|
|
29
|
+
|
|
30
|
+
**Access patterns:**
|
|
31
|
+
|
|
32
|
+
1. **Direct URL**: `GET /blog/my-post.md` — proxy rewrites to `/api/blog/my-post.md`, route handler validates `.md`, strips it, serves markdown
|
|
33
|
+
2. **Accept header**: `GET /blog/my-post` with `Accept: text/markdown` — proxy rewrites to `/api/blog/my-post.md`
|
|
34
|
+
3. **Without `.md`**: `GET /blog/my-post` — serves the HTML page (if `page.tsx` exists), not markdown
|
|
35
|
+
4. **Discovery**: `GET /sitemap.md` — lists all available markdown content
|
|
36
|
+
|
|
37
|
+
## Step-by-Step: Add a New Content Type
|
|
38
|
+
|
|
39
|
+
Example: adding a `blog` content type backed by Sanity document type `blog`.
|
|
40
|
+
|
|
41
|
+
### 1. Check your Sanity schema
|
|
42
|
+
|
|
43
|
+
Look at your schema definition in `lib/integrations/sanity/schemas/` to understand the fields. Every field you want in the markdown output needs to be queried and rendered.
|
|
44
|
+
|
|
45
|
+
### 2. Add a GROQ query
|
|
46
|
+
|
|
47
|
+
In `lib/integrations/sanity/queries.ts`:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
export const BLOG_QUERY = defineQuery(`
|
|
51
|
+
*[_type == "blog" && slug.current == $slug][0] {
|
|
52
|
+
_id,
|
|
53
|
+
title,
|
|
54
|
+
slug,
|
|
55
|
+
excerpt,
|
|
56
|
+
author,
|
|
57
|
+
publishedAt,
|
|
58
|
+
${richTextWithLinks},
|
|
59
|
+
tags,
|
|
60
|
+
metadata,
|
|
61
|
+
_updatedAt
|
|
62
|
+
}
|
|
63
|
+
`;
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Rules:
|
|
67
|
+
- Always include `_id`, `title`, `slug`, `_updatedAt`
|
|
68
|
+
- Use the existing `richTextWithLinks` fragment for any `content` field with Portable Text — it resolves internal links within rich text markup
|
|
69
|
+
- Use `$slug` parameter — `SanityFetch` passes it automatically
|
|
70
|
+
- Include all fields you want to render in the markdown output
|
|
71
|
+
|
|
72
|
+
### 3. Create the route handler
|
|
73
|
+
|
|
74
|
+
Create `app/api/blog/[slug]/route.ts`:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { NextResponse } from "next/server";
|
|
78
|
+
import { portableTextToMarkdown } from "@/lib/utils/portable-text-to-markdown";
|
|
79
|
+
import { isSanityConfigured } from "@/lib/integrations/check-integration";
|
|
80
|
+
import { BLOG_QUERY } from "@/lib/integrations/sanity/queries";
|
|
81
|
+
import { SanityFetch } from "@/lib/integrations/sanity/live";
|
|
82
|
+
|
|
83
|
+
export async function GET(
|
|
84
|
+
_request: Request,
|
|
85
|
+
props: { params: Promise<{ slug: string }> },
|
|
86
|
+
) {
|
|
87
|
+
const { slug: rawSlug } = await props.params;
|
|
88
|
+
|
|
89
|
+
if (!rawSlug.endsWith(".md")) {
|
|
90
|
+
return new NextResponse(null, { status: 404 });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const slug = rawSlug.slice(0, -3);
|
|
94
|
+
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000";
|
|
95
|
+
|
|
96
|
+
if (!isSanityConfigured()) {
|
|
97
|
+
return new NextResponse("# 404 Not Found\n\nCMS not configured.", {
|
|
98
|
+
status: 404,
|
|
99
|
+
headers: { "Content-Type": "text/markdown; charset=utf-8" },
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const blog = await SanityFetch(slug, BLOG_QUERY, "Blog Post");
|
|
105
|
+
|
|
106
|
+
// SanityFetch returns NextResponse on error — pass it through
|
|
107
|
+
if (blog instanceof NextResponse) return blog;
|
|
108
|
+
|
|
109
|
+
const parts: (string | null)[] = [
|
|
110
|
+
`# ${blog.title}`,
|
|
111
|
+
"",
|
|
112
|
+
blog.excerpt ? `> ${blog.excerpt}` : null,
|
|
113
|
+
"",
|
|
114
|
+
blog.author ? `**Author:** ${blog.author}` : null,
|
|
115
|
+
blog.publishedAt
|
|
116
|
+
? `**Published:** ${new Date(blog.publishedAt).toLocaleDateString()}`
|
|
117
|
+
: null,
|
|
118
|
+
blog._updatedAt
|
|
119
|
+
? `**Updated:** ${new Date(blog._updatedAt).toLocaleDateString()}`
|
|
120
|
+
: null,
|
|
121
|
+
blog.tags?.length ? `**Tags:** ${blog.tags.join(", ")}` : null,
|
|
122
|
+
"",
|
|
123
|
+
"---",
|
|
124
|
+
"",
|
|
125
|
+
portableTextToMarkdown(blog.content),
|
|
126
|
+
"",
|
|
127
|
+
"---",
|
|
128
|
+
"",
|
|
129
|
+
`[View all content](${baseUrl}/sitemap.md)`,
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
const markdown = parts.filter((part) => part !== null).join("\n");
|
|
133
|
+
|
|
134
|
+
return new NextResponse(markdown, {
|
|
135
|
+
headers: {
|
|
136
|
+
"Content-Type": "text/markdown; charset=utf-8",
|
|
137
|
+
Vary: "Accept",
|
|
138
|
+
"X-Content-Type-Options": "nosniff",
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error("Error fetching blog markdown:", error);
|
|
143
|
+
return new NextResponse("# 500 Error\n\nFailed to fetch blog post.", {
|
|
144
|
+
status: 500,
|
|
145
|
+
headers: { "Content-Type": "text/markdown; charset=utf-8" },
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Route handler rules:**
|
|
152
|
+
|
|
153
|
+
- Always validate `rawSlug.endsWith(".md")` first — return 404 if missing
|
|
154
|
+
- Strip `.md` with `rawSlug.slice(0, -3)` to get the actual slug
|
|
155
|
+
- Check `isSanityConfigured()` before querying
|
|
156
|
+
- Use `SanityFetch(slug, query, label)` — it handles 404/500/503 errors
|
|
157
|
+
- Check `if (result instanceof NextResponse) return result` to propagate error responses
|
|
158
|
+
- Use `portableTextToMarkdown()` for any Portable Text / rich text field
|
|
159
|
+
- Include a link to `/sitemap.md` at the bottom
|
|
160
|
+
- Set `Content-Type: text/markdown; charset=utf-8` on all responses
|
|
161
|
+
- Map **every schema field** to markdown — dates formatted, arrays joined, booleans as text
|
|
162
|
+
|
|
163
|
+
### 4. Update the link resolver
|
|
164
|
+
|
|
165
|
+
In `lib/integrations/sanity/utils/link.ts`, add the new type to `CONTENT_TYPE_TO_PATH`:
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
export const CONTENT_TYPE_TO_PATH: Record<string, string> = {
|
|
169
|
+
article: "/blog",
|
|
170
|
+
blog: "/blog", // add this
|
|
171
|
+
};
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
This ensures `resolveMarkdownUrl` generates correct `.md` links when other content types reference blog posts via internal links.
|
|
175
|
+
|
|
176
|
+
### 5. Update the sitemap
|
|
177
|
+
|
|
178
|
+
In `app/sitemap.md/route.ts`, add a query and section for the new type:
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
// In the fetch block, add:
|
|
182
|
+
const [articles, blogs] = await Promise.all([
|
|
183
|
+
// ... existing queries ...
|
|
184
|
+
client.fetch<SanityDoc[]>(
|
|
185
|
+
groq`*[_type == "blog" && defined(slug.current)] | order(publishedAt desc) {
|
|
186
|
+
title,
|
|
187
|
+
slug,
|
|
188
|
+
_updatedAt
|
|
189
|
+
}`,
|
|
190
|
+
),
|
|
191
|
+
]);
|
|
192
|
+
|
|
193
|
+
// After the existing sections, add:
|
|
194
|
+
if (blogs.length > 0) {
|
|
195
|
+
parts.push("## Blog Posts", "");
|
|
196
|
+
for (const blog of blogs) {
|
|
197
|
+
parts.push(
|
|
198
|
+
`- [${blog.title}](${baseUrl}/blog/${blog.slug.current}.md)`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
parts.push("");
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Field to Markdown Mapping
|
|
206
|
+
|
|
207
|
+
| Schema Field Type | Markdown Output |
|
|
208
|
+
|---|---|
|
|
209
|
+
| `string` (title) | `# ${value}` |
|
|
210
|
+
| `text` (excerpt) | `> ${value}` |
|
|
211
|
+
| `string` (author) | `**Author:** ${value}` |
|
|
212
|
+
| `datetime` | `**Published:** ${new Date(value).toLocaleDateString()}` |
|
|
213
|
+
| `array of strings` (tags) | `**Tags:** ${value.join(", ")}` |
|
|
214
|
+
| `array of blocks` (content) | `portableTextToMarkdown(value)` |
|
|
215
|
+
| `image` | Handled automatically inside `portableTextToMarkdown` |
|
|
216
|
+
| `reference` (internal link) | `[${text}](resolveMarkdownUrl(ref, baseUrl))` |
|
|
217
|
+
| `url` (external link) | `[${text}](${url})` |
|
|
218
|
+
| `slug` | Not rendered directly — used for routing |
|
|
219
|
+
| `object` (metadata) | Not rendered — used for SEO only |
|
|
220
|
+
|
|
221
|
+
## Markdown Structure Convention
|
|
222
|
+
|
|
223
|
+
Every route handler should produce this structure:
|
|
224
|
+
|
|
225
|
+
```markdown
|
|
226
|
+
# Title
|
|
227
|
+
|
|
228
|
+
**Field:** value
|
|
229
|
+
**Field:** value
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
[main content from Portable Text]
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
[View all content](baseUrl/sitemap.md)
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
- Title as `h1`
|
|
241
|
+
- Metadata fields as bold key-value pairs
|
|
242
|
+
- Horizontal rules to separate metadata from content
|
|
243
|
+
- Footer link back to sitemap for navigation
|
|
244
|
+
|
|
245
|
+
## SanityFetch Helper
|
|
246
|
+
|
|
247
|
+
The `SanityFetch` function in `lib/integrations/sanity/live/index.tsx` wraps `sanityFetch` with standardized markdown error handling:
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
const result = await SanityFetch(slug, yourQuery, "Label");
|
|
251
|
+
if (result instanceof NextResponse) return result; // Error response (404, 500, 503)
|
|
252
|
+
// result is now your data
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
It returns:
|
|
256
|
+
- **503** — CMS client unavailable
|
|
257
|
+
- **404** — Document not found (null data)
|
|
258
|
+
- **500** — Query execution error
|
|
259
|
+
- **Data** — The fetched document on success
|
|
260
|
+
|
|
261
|
+
## Checklist
|
|
262
|
+
|
|
263
|
+
When adding a new content type, verify all of these:
|
|
264
|
+
|
|
265
|
+
- [ ] GROQ query in `queries.ts` fetches all schema fields
|
|
266
|
+
- [ ] Route handler at `app/api/{type}/[slug]/route.ts` with `.md` extension validation
|
|
267
|
+
- [ ] Route added to `markdown-proxy.config.ts` (`mdExtensionRoutes`)
|
|
268
|
+
- [ ] `CONTENT_TYPE_TO_PATH` updated in `utils/link.ts`
|
|
269
|
+
- [ ] Sitemap section added in `app/sitemap.md/route.ts`
|
|
270
|
+
- [ ] Test: `curl http://localhost:3000/{type}/{slug}.md` returns markdown
|
|
271
|
+
- [ ] Test: `curl http://localhost:3000/sitemap.md` lists the new content
|
|
272
|
+
- [ ] Test: `curl http://localhost:3000/{type}/nonexistent.md` returns 404
|
|
273
|
+
- [ ] Test: `curl http://localhost:3000/{type}/{slug}` returns 404 (no `.md` = no markdown)
|
|
@@ -22,7 +22,7 @@ NEXT_PUBLIC_SANITY_API_VERSION="2024-03-15"
|
|
|
22
22
|
## Quick Start
|
|
23
23
|
|
|
24
24
|
1. Access Studio at `/studio`
|
|
25
|
-
2. Create content (
|
|
25
|
+
2. Create content (Articles)
|
|
26
26
|
3. Click "Present" for visual editing
|
|
27
27
|
|
|
28
28
|
## Usage
|
|
@@ -31,12 +31,12 @@ NEXT_PUBLIC_SANITY_API_VERSION="2024-03-15"
|
|
|
31
31
|
|
|
32
32
|
```tsx
|
|
33
33
|
import { sanityFetch } from 'next-sanity/live'
|
|
34
|
-
import {
|
|
34
|
+
import { ARTICLE_QUERY } from '@/lib/integrations/sanity/queries'
|
|
35
35
|
|
|
36
36
|
export default async function Page({ params }) {
|
|
37
|
-
const { data } = await sanityFetch({
|
|
38
|
-
query:
|
|
39
|
-
params: { slug: params.slug }
|
|
37
|
+
const { data } = await sanityFetch({
|
|
38
|
+
query: ARTICLE_QUERY,
|
|
39
|
+
params: { slug: params.slug }
|
|
40
40
|
})
|
|
41
41
|
return <YourComponent data={data} />
|
|
42
42
|
}
|
|
@@ -48,11 +48,11 @@ For `generateStaticParams` or other build-time functions, use the client directl
|
|
|
48
48
|
|
|
49
49
|
```tsx
|
|
50
50
|
import { client } from '@/lib/integrations/sanity/client'
|
|
51
|
-
import {
|
|
51
|
+
import { ALL_ARTICLES_QUERY } from '@/lib/integrations/sanity/queries'
|
|
52
52
|
|
|
53
53
|
export async function generateStaticParams() {
|
|
54
54
|
if (!client) return []
|
|
55
|
-
const data = await client.fetch(
|
|
55
|
+
const data = await client.fetch(ALL_ARTICLES_QUERY)
|
|
56
56
|
return data.map((item) => ({ slug: item.slug?.current ?? '' }))
|
|
57
57
|
}
|
|
58
58
|
```
|
|
@@ -93,8 +93,8 @@ import { SanityImage } from '@/components/ui/sanity-image'
|
|
|
93
93
|
import { generateSanityMetadata } from '@/lib/utils/metadata'
|
|
94
94
|
|
|
95
95
|
export async function generateMetadata({ params }) {
|
|
96
|
-
const { data } = await sanityFetch({ query:
|
|
97
|
-
return generateSanityMetadata({ document: data, url: `/
|
|
96
|
+
const { data } = await sanityFetch({ query: ARTICLE_QUERY, params })
|
|
97
|
+
return generateSanityMetadata({ document: data, url: `/blog/${params.slug}` })
|
|
98
98
|
}
|
|
99
99
|
```
|
|
100
100
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { PortableText, type
|
|
1
|
+
import { PortableText, type PortableTextProps } from "@portabletext/react";
|
|
2
2
|
import { Link } from "@/components/ui/link";
|
|
3
3
|
import { SanityImage } from "@/components/ui/sanity-image";
|
|
4
4
|
|
|
5
5
|
interface RichTextProps {
|
|
6
|
-
content:
|
|
6
|
+
content: PortableTextProps["value"];
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
interface LinkFieldData {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { defineLive } from "next-sanity/live";
|
|
2
|
+
import { NextResponse } from "next/server";
|
|
2
3
|
import { isSanityConfigured } from "@/lib/integrations/check-integration";
|
|
3
4
|
import { client } from "../client";
|
|
4
5
|
import { sanityToken } from "../env";
|
|
@@ -11,14 +12,14 @@ import { sanityToken } from "../env";
|
|
|
11
12
|
*/
|
|
12
13
|
const isConfigured = isSanityConfigured() && client;
|
|
13
14
|
|
|
14
|
-
const liveExports =
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
const liveExports =
|
|
16
|
+
isConfigured && client
|
|
17
|
+
? defineLive({
|
|
18
|
+
client,
|
|
19
|
+
browserToken: sanityToken,
|
|
20
|
+
serverToken: sanityToken,
|
|
21
|
+
})
|
|
22
|
+
: null;
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Standard sanityFetch function from next-sanity/live.
|
|
@@ -32,3 +33,47 @@ export const sanityFetch =
|
|
|
32
33
|
* Returns null when Sanity is not configured.
|
|
33
34
|
*/
|
|
34
35
|
export const SanityLive = liveExports?.SanityLive ?? (() => null);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Custom SanityFetch helper for Markdown routes.
|
|
39
|
+
* Returns the data or a NextResponse with an error message.
|
|
40
|
+
*/
|
|
41
|
+
export const SanityFetch = async <T,>(
|
|
42
|
+
slug: string,
|
|
43
|
+
query: string,
|
|
44
|
+
label = "Content",
|
|
45
|
+
): Promise<T | NextResponse> => {
|
|
46
|
+
if (!client) {
|
|
47
|
+
return new NextResponse(`# 503 Unavailable\n\nCMS client unavailable.`, {
|
|
48
|
+
status: 503,
|
|
49
|
+
headers: { "Content-Type": "text/markdown; charset=utf-8" },
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const { data } = await sanityFetch({
|
|
55
|
+
query,
|
|
56
|
+
params: { slug },
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const result = data as T | null;
|
|
60
|
+
|
|
61
|
+
if (!result) {
|
|
62
|
+
return new NextResponse(`# 404 Not Found\n\n${label} not found.`, {
|
|
63
|
+
status: 404,
|
|
64
|
+
headers: { "Content-Type": "text/markdown; charset=utf-8" },
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return result;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error(`Error fetching ${label.toLowerCase()}:`, error);
|
|
71
|
+
return new NextResponse(
|
|
72
|
+
`# 500 Error\n\nFailed to fetch ${label.toLowerCase()}.`,
|
|
73
|
+
{
|
|
74
|
+
status: 500,
|
|
75
|
+
headers: { "Content-Type": "text/markdown; charset=utf-8" },
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export interface MarkdownRoute {
|
|
2
|
+
/** Regex to match the public-facing URL */
|
|
3
|
+
regex: RegExp;
|
|
4
|
+
/** API route path template (under /api/) */
|
|
5
|
+
apiPath: string;
|
|
6
|
+
/** Public markdown URL template (for Link headers) */
|
|
7
|
+
publicPath: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Routes that serve markdown versions for AI agents.
|
|
12
|
+
* Add entries here to enable markdown proxy for new content types.
|
|
13
|
+
*
|
|
14
|
+
* How it works:
|
|
15
|
+
* - `regex`: match the public-facing URL (e.g. /blog/slug.md or /about)
|
|
16
|
+
* - `apiPath`: the internal API route that serves the markdown (e.g. /api/blog/slug.md)
|
|
17
|
+
* - Proxy rewrites .md URLs and Accept: text/markdown requests to the API route
|
|
18
|
+
* - Route handlers live under /api/ so page.tsx can coexist at the public path
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** .md extension routes — rewrite to API endpoints */
|
|
22
|
+
export const mdExtensionRoutes: MarkdownRoute[] = [
|
|
23
|
+
{
|
|
24
|
+
regex: /^\/blog\/([^/]+)\.md$/,
|
|
25
|
+
apiPath: "/api/blog/[slug].md",
|
|
26
|
+
publicPath: "/blog/[slug].md",
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/** Accept: text/markdown routes — content negotiation for HTML pages */
|
|
31
|
+
export const acceptHeaderRoutes: MarkdownRoute[] = [
|
|
32
|
+
{
|
|
33
|
+
regex: /^\/blog\/([^/.]+)$/,
|
|
34
|
+
apiPath: "/api/blog/[slug].md",
|
|
35
|
+
publicPath: "/blog/[slug].md",
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Paths the proxy should never process.
|
|
41
|
+
* Studio, static files, Next.js internals.
|
|
42
|
+
*/
|
|
43
|
+
export const ignoredPaths = [
|
|
44
|
+
"/studio",
|
|
45
|
+
"/api/",
|
|
46
|
+
"/_next/",
|
|
47
|
+
"/favicon.ico",
|
|
48
|
+
"/sitemap.md",
|
|
49
|
+
];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { defineQuery } from "next-sanity";
|
|
2
2
|
|
|
3
3
|
// Helper for rich text content with link projections
|
|
4
4
|
const richTextWithLinks = `
|
|
@@ -14,41 +14,8 @@ const richTextWithLinks = `
|
|
|
14
14
|
}
|
|
15
15
|
`;
|
|
16
16
|
|
|
17
|
-
const linkWithLabel = `
|
|
18
|
-
link {
|
|
19
|
-
...,
|
|
20
|
-
internalLink->{_type, slug, title}
|
|
21
|
-
}
|
|
22
|
-
`;
|
|
23
|
-
|
|
24
|
-
// Page queries
|
|
25
|
-
export const pageQuery = groq`
|
|
26
|
-
*[_type == "page" && slug.current == $slug][0] {
|
|
27
|
-
_id,
|
|
28
|
-
title,
|
|
29
|
-
slug,
|
|
30
|
-
${richTextWithLinks},
|
|
31
|
-
${linkWithLabel},
|
|
32
|
-
metadata,
|
|
33
|
-
publishedAt,
|
|
34
|
-
_updatedAt
|
|
35
|
-
}
|
|
36
|
-
`;
|
|
37
|
-
|
|
38
|
-
export const pageByIdQuery = groq`
|
|
39
|
-
*[_type == "page" && _id == $id][0] {
|
|
40
|
-
_id,
|
|
41
|
-
title,
|
|
42
|
-
slug,
|
|
43
|
-
${richTextWithLinks},
|
|
44
|
-
metadata,
|
|
45
|
-
publishedAt,
|
|
46
|
-
_updatedAt
|
|
47
|
-
}
|
|
48
|
-
`;
|
|
49
|
-
|
|
50
17
|
// Article queries
|
|
51
|
-
export const
|
|
18
|
+
export const ARTICLE_QUERY = defineQuery(`
|
|
52
19
|
*[_type == "article" && slug.current == $slug][0] {
|
|
53
20
|
_id,
|
|
54
21
|
title,
|
|
@@ -63,9 +30,9 @@ export const articleQuery = groq`
|
|
|
63
30
|
metadata,
|
|
64
31
|
_updatedAt
|
|
65
32
|
}
|
|
66
|
-
|
|
33
|
+
`);
|
|
67
34
|
|
|
68
|
-
export const
|
|
35
|
+
export const ALL_ARTICLES_QUERY = defineQuery(`
|
|
69
36
|
*[_type == "article"] | order(publishedAt desc) {
|
|
70
37
|
_id,
|
|
71
38
|
title,
|
|
@@ -79,9 +46,9 @@ export const allArticlesQuery = groq`
|
|
|
79
46
|
metadata,
|
|
80
47
|
_updatedAt
|
|
81
48
|
}
|
|
82
|
-
|
|
49
|
+
`);
|
|
83
50
|
|
|
84
|
-
export const
|
|
51
|
+
export const ARTICLE_BY_ID_QUERY = defineQuery(`
|
|
85
52
|
*[_type == "article" && _id == $id][0] {
|
|
86
53
|
_id,
|
|
87
54
|
title,
|
|
@@ -96,4 +63,4 @@ export const articleByIdQuery = groq`
|
|
|
96
63
|
metadata,
|
|
97
64
|
_updatedAt
|
|
98
65
|
}
|
|
99
|
-
|
|
66
|
+
`);
|