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.
Files changed (75) hide show
  1. package/README.md +1 -1
  2. package/dist/helpers/create/index.d.ts +2 -1
  3. package/dist/helpers/create/index.d.ts.map +1 -1
  4. package/dist/helpers/create/index.js +16 -5
  5. package/dist/helpers/create/index.js.map +1 -1
  6. package/dist/helpers/create/init-git.d.ts +12 -0
  7. package/dist/helpers/create/init-git.d.ts.map +1 -0
  8. package/dist/helpers/create/init-git.js +34 -0
  9. package/dist/helpers/create/init-git.js.map +1 -0
  10. package/dist/helpers/create/setup-agent.d.ts +3 -2
  11. package/dist/helpers/create/setup-agent.d.ts.map +1 -1
  12. package/dist/helpers/create/setup-agent.js +42 -31
  13. package/dist/helpers/create/setup-agent.js.map +1 -1
  14. package/dist/helpers/create/setup-sanity.d.ts.map +1 -1
  15. package/dist/helpers/create/setup-sanity.js +31 -25
  16. package/dist/helpers/create/setup-sanity.js.map +1 -1
  17. package/dist/helpers/create/update-package.d.ts.map +1 -1
  18. package/dist/helpers/create/update-package.js +10 -0
  19. package/dist/helpers/create/update-package.js.map +1 -1
  20. package/dist/helpers/integrate/index.d.ts.map +1 -1
  21. package/dist/helpers/integrate/index.js +27 -13
  22. package/dist/helpers/integrate/index.js.map +1 -1
  23. package/dist/helpers/integrate/merge-config.d.ts.map +1 -1
  24. package/dist/helpers/integrate/merge-config.js +5 -0
  25. package/dist/helpers/integrate/merge-config.js.map +1 -1
  26. package/dist/helpers/integrate/merge-orchestrator.js +1 -1
  27. package/dist/helpers/integrate/merge-orchestrator.js.map +1 -1
  28. package/dist/helpers/integrate/sanity/config.d.ts.map +1 -1
  29. package/dist/helpers/integrate/sanity/config.js +12 -1
  30. package/dist/helpers/integrate/sanity/config.js.map +1 -1
  31. package/dist/helpers/integrate/sanity/mergers/sitemap-merger.d.ts.map +1 -1
  32. package/dist/helpers/integrate/sanity/mergers/sitemap-merger.js +2 -21
  33. package/dist/helpers/integrate/sanity/mergers/sitemap-merger.js.map +1 -1
  34. package/dist/index.js +48 -19
  35. package/dist/index.js.map +1 -1
  36. package/index.js +4 -3
  37. package/package.json +7 -2
  38. package/src/helpers/integrate/sanity/files/app/api/blog/[slug]/route.ts +75 -0
  39. package/src/helpers/integrate/sanity/files/app/blog/[slug]/page.tsx +56 -0
  40. package/src/helpers/integrate/sanity/files/app/sitemap.md/route.ts +82 -0
  41. package/src/helpers/integrate/sanity/files/app/sitemap.ts +2 -21
  42. package/src/helpers/integrate/sanity/files/lib/integrations/README.md +1 -1
  43. package/src/helpers/integrate/sanity/files/lib/integrations/sanity/MARKDOWN-PROXY.md +273 -0
  44. package/src/helpers/integrate/sanity/files/lib/integrations/sanity/README.md +9 -9
  45. package/src/helpers/integrate/sanity/files/lib/integrations/sanity/components/rich-text.tsx +2 -2
  46. package/src/helpers/integrate/sanity/files/lib/integrations/sanity/live/index.tsx +53 -8
  47. package/src/helpers/integrate/sanity/files/lib/integrations/sanity/markdown-proxy.config.ts +49 -0
  48. package/src/helpers/integrate/sanity/files/lib/integrations/sanity/queries.ts +7 -40
  49. package/src/helpers/integrate/sanity/files/lib/integrations/sanity/sanity.cli.ts +5 -0
  50. package/src/helpers/integrate/sanity/files/lib/integrations/sanity/sanity.config.ts +2 -22
  51. package/src/helpers/integrate/sanity/files/lib/integrations/sanity/sanity.types.ts +1 -24
  52. package/src/helpers/integrate/sanity/files/lib/integrations/sanity/schema.json +2 -124
  53. package/src/helpers/integrate/sanity/files/lib/integrations/sanity/schemas/index.ts +1 -3
  54. package/src/helpers/integrate/sanity/files/lib/integrations/sanity/schemas/link.ts +2 -2
  55. package/src/helpers/integrate/sanity/files/lib/integrations/sanity/utils/link.ts +25 -2
  56. package/src/helpers/integrate/sanity/files/lib/scripts/generate-page.ts +17 -30
  57. package/src/helpers/integrate/sanity/files/lib/utils/metadata.ts +10 -10
  58. package/src/helpers/integrate/sanity/files/lib/utils/portable-text-to-markdown.ts +24 -0
  59. package/src/helpers/integrate/sanity/files/proxy.ts +66 -0
  60. package/src/templates/next-default/components/ui/image/image.module.css +5 -0
  61. package/src/templates/next-default/components/ui/image/index.tsx +1 -2
  62. package/src/templates/next-default/lib/scripts/generate.ts +2 -2
  63. package/src/templates/next-default/lib/utils/metadata.ts +1 -1
  64. package/src/templates/next-default/next.config.ts +0 -1
  65. package/src/templates/next-experiments/components/ui/image/image.module.css +5 -0
  66. package/src/templates/next-experiments/components/ui/image/index.tsx +3 -2
  67. package/src/templates/next-experiments/lib/scripts/generate.ts +2 -2
  68. package/src/templates/next-experiments/lib/utils/metadata.ts +1 -1
  69. package/src/templates/next-experiments/next.config.ts +0 -1
  70. package/src/templates/next-webgl/components/ui/image/image.module.css +5 -0
  71. package/src/templates/next-webgl/components/ui/image/index.tsx +1 -2
  72. package/src/templates/next-webgl/lib/scripts/generate.ts +2 -2
  73. package/src/templates/next-webgl/lib/utils/metadata.ts +1 -1
  74. package/src/templates/next-webgl/next.config.ts +0 -1
  75. 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 pages if Sanity is configured
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, ...pageEntries, ...articleEntries];
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: pageQuery })
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 (Pages, Articles)
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 { pageQuery } from '@/lib/integrations/sanity/queries'
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: pageQuery,
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 { allArticlesQuery } from '@/lib/integrations/sanity/queries'
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(allArticlesQuery)
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: pageQuery, params })
97
- return generateSanityMetadata({ document: data, url: `/page/${params.slug}` })
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 PortableTextBlock } from "@portabletext/react";
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: PortableTextBlock[];
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 = isConfigured
15
- ? defineLive({
16
- // biome-ignore lint/style/noNonNullAssertion: client is checked via isConfigured above
17
- client: client!,
18
- browserToken: sanityToken,
19
- serverToken: sanityToken,
20
- })
21
- : null;
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 { groq } from "next-sanity";
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 articleQuery = groq`
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 allArticlesQuery = groq`
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 articleByIdQuery = groq`
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
+ `);
@@ -10,6 +10,11 @@ export default defineCliConfig({
10
10
  project: {
11
11
  basePath: "/studio",
12
12
  },
13
+ typegen: {
14
+ path: "./queries.ts",
15
+ schema: "./schema.json",
16
+ generates: "./sanity.types.ts",
17
+ },
13
18
  vite: {
14
19
  resolve: {
15
20
  alias: {