bsmnt 0.0.0
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/.changeset/2026-02-11-test-patch-bump.md +5 -0
- package/.changeset/README.md +10 -0
- package/.changeset/config.json +16 -0
- package/.cursor/rules/README.md +184 -0
- package/.cursor/rules/architecture.mdc +437 -0
- package/.cursor/rules/components.mdc +436 -0
- package/.cursor/rules/integrations.mdc +447 -0
- package/.cursor/rules/main.mdc +278 -0
- package/.cursor/rules/styling.mdc +433 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +14 -0
- package/.github/workflows/.gitkeep +0 -0
- package/.github/workflows/ci.yml +37 -0
- package/.github/workflows/release.yml +54 -0
- package/.tldr/cache/call_graph.json +7 -0
- package/.tldr/languages.json +6 -0
- package/.tldr/status +1 -0
- package/.tldrignore +84 -0
- package/.vscode/extensions.json +20 -0
- package/.vscode/settings.json +98 -0
- package/CHANGELOG.md +13 -0
- package/CLAUDE.md +138 -0
- package/README.md +176 -0
- package/bin/index.js +262 -0
- package/biome.json +44 -0
- package/bun.lock +496 -0
- package/changelog/04-02-26.md +86 -0
- package/changelog/05-02-26.md +101 -0
- package/changelog/09-02-26.md +83 -0
- package/docs/fix-studio-hydration.md +46 -0
- package/docs/plans/2026-01-29-sanity-smart-merge-design.md +196 -0
- package/docs/plans/2026-01-29-sanity-smart-merge-implementation.md +695 -0
- package/docs/sanity-setup-steps.md +199 -0
- package/integrations/basehub/README.md +3 -0
- package/integrations/sanity/app/api/draft-mode/disable/route.ts +7 -0
- package/integrations/sanity/app/api/draft-mode/enable/route.ts +21 -0
- package/integrations/sanity/app/api/revalidate/route.ts +37 -0
- package/integrations/sanity/app/layout.tsx +111 -0
- package/integrations/sanity/app/sitemap.ts +80 -0
- package/integrations/sanity/app/studio/[[...tool]]/page.tsx +8 -0
- package/integrations/sanity/app/studio/layout.tsx +7 -0
- package/integrations/sanity/components/ui/sanity-image/index.tsx +37 -0
- package/integrations/sanity/lib/integrations/README.md +58 -0
- package/integrations/sanity/lib/integrations/check-integration.ts +62 -0
- package/integrations/sanity/lib/integrations/sanity/README.md +144 -0
- package/integrations/sanity/lib/integrations/sanity/client.ts +30 -0
- package/integrations/sanity/lib/integrations/sanity/components/disable-draft-mode.tsx +29 -0
- package/integrations/sanity/lib/integrations/sanity/components/rich-text.tsx +73 -0
- package/integrations/sanity/lib/integrations/sanity/env.ts +38 -0
- package/integrations/sanity/lib/integrations/sanity/live/index.tsx +34 -0
- package/integrations/sanity/lib/integrations/sanity/queries.ts +99 -0
- package/integrations/sanity/lib/integrations/sanity/sanity.cli.ts +20 -0
- package/integrations/sanity/lib/integrations/sanity/sanity.config.ts +94 -0
- package/integrations/sanity/lib/integrations/sanity/sanity.types.ts +337 -0
- package/integrations/sanity/lib/integrations/sanity/schema.json +1850 -0
- package/integrations/sanity/lib/integrations/sanity/schemas/article.ts +132 -0
- package/integrations/sanity/lib/integrations/sanity/schemas/example.ts +203 -0
- package/integrations/sanity/lib/integrations/sanity/schemas/index.ts +37 -0
- package/integrations/sanity/lib/integrations/sanity/schemas/link.ts +127 -0
- package/integrations/sanity/lib/integrations/sanity/schemas/metadata.ts +68 -0
- package/integrations/sanity/lib/integrations/sanity/schemas/navigation.ts +39 -0
- package/integrations/sanity/lib/integrations/sanity/schemas/page.ts +77 -0
- package/integrations/sanity/lib/integrations/sanity/schemas/richText.ts +59 -0
- package/integrations/sanity/lib/integrations/sanity/structure.ts +5 -0
- package/integrations/sanity/lib/integrations/sanity/utils/image.ts +11 -0
- package/integrations/sanity/lib/integrations/sanity/utils/link.ts +61 -0
- package/integrations/sanity/lib/scripts/copy-sanity-mcp.ts +23 -0
- package/integrations/sanity/lib/scripts/generate-page.ts +310 -0
- package/integrations/sanity/lib/utils/metadata.ts +190 -0
- package/layers/experiment/components/layout/header/index.tsx +58 -0
- package/layers/experiment/components/layout/navigation-menu.tsx +127 -0
- package/layers/experiment/lib/constants.ts +12 -0
- package/layers/webgl/app/page.tsx +10 -0
- package/layers/webgl/components/webgl/canvas/dynamic.tsx +34 -0
- package/layers/webgl/components/webgl/canvas/index.tsx +43 -0
- package/layers/webgl/components/webgl/components/scene/index.tsx +21 -0
- package/layers/webgpu/.gitkeep +0 -0
- package/package.json +44 -0
- package/plugins/README.md +21 -0
- package/plugins/no-anchor-element.grit +11 -0
- package/plugins/no-relative-parent-imports.grit +6 -0
- package/plugins/no-unnecessary-forwardref.grit +5 -0
- package/src/commands/add-integration.js +325 -0
- package/src/commands/create.js +415 -0
- package/src/commands/setup-sanity.js +426 -0
- package/src/commands/worktree.js +805 -0
- package/src/mergers/check-integration-merger.js +105 -0
- package/src/mergers/config.js +137 -0
- package/src/mergers/index.js +355 -0
- package/src/mergers/layout-merger.js +223 -0
- package/src/mergers/next-config-merger.js +63 -0
- package/src/mergers/sitemap-merger.js +121 -0
- package/tasks/prd-next-starter-dynamic-layers.md +184 -0
- package/tasks/prd.json +153 -0
- package/tasks/progress.txt +115 -0
- package/template-hooks/use-battery.ts +126 -0
- package/template-hooks/use-device-perf.ts +184 -0
- package/template-hooks/use-intersection-observer.ts +32 -0
- package/template-hooks/use-media.ts +33 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { defineField, defineType } from "sanity";
|
|
2
|
+
|
|
3
|
+
const articleFields = [
|
|
4
|
+
defineField({
|
|
5
|
+
name: "title",
|
|
6
|
+
title: "Title",
|
|
7
|
+
type: "string",
|
|
8
|
+
description: "The title of the article",
|
|
9
|
+
validation: (Rule) => Rule.required(),
|
|
10
|
+
}),
|
|
11
|
+
defineField({
|
|
12
|
+
name: "excerpt",
|
|
13
|
+
title: "Excerpt",
|
|
14
|
+
type: "text",
|
|
15
|
+
rows: 3,
|
|
16
|
+
description: "A brief summary of the article",
|
|
17
|
+
validation: (Rule) => Rule.max(200),
|
|
18
|
+
}),
|
|
19
|
+
defineField({
|
|
20
|
+
name: "featuredImage",
|
|
21
|
+
title: "Featured Image",
|
|
22
|
+
type: "image",
|
|
23
|
+
description: "Main image for the article",
|
|
24
|
+
options: {
|
|
25
|
+
hotspot: true,
|
|
26
|
+
},
|
|
27
|
+
fields: [
|
|
28
|
+
{
|
|
29
|
+
name: "alt",
|
|
30
|
+
title: "Alt Text",
|
|
31
|
+
type: "string",
|
|
32
|
+
description: "Alternative text for screen readers",
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
}),
|
|
36
|
+
defineField({
|
|
37
|
+
name: "content",
|
|
38
|
+
title: "Content",
|
|
39
|
+
type: "richText",
|
|
40
|
+
description: "The main content of the article",
|
|
41
|
+
}),
|
|
42
|
+
defineField({
|
|
43
|
+
name: "categories",
|
|
44
|
+
title: "Categories",
|
|
45
|
+
type: "array",
|
|
46
|
+
of: [{ type: "string" }],
|
|
47
|
+
description: "Categories for this article",
|
|
48
|
+
options: {
|
|
49
|
+
layout: "tags",
|
|
50
|
+
},
|
|
51
|
+
}),
|
|
52
|
+
defineField({
|
|
53
|
+
name: "tags",
|
|
54
|
+
title: "Tags",
|
|
55
|
+
type: "array",
|
|
56
|
+
of: [{ type: "string" }],
|
|
57
|
+
description: "Tags for this article",
|
|
58
|
+
options: {
|
|
59
|
+
layout: "tags",
|
|
60
|
+
},
|
|
61
|
+
}),
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
export const article = defineType({
|
|
65
|
+
name: "article",
|
|
66
|
+
title: "Article",
|
|
67
|
+
type: "document",
|
|
68
|
+
fields: [
|
|
69
|
+
defineField({
|
|
70
|
+
name: "slug",
|
|
71
|
+
title: "Slug",
|
|
72
|
+
type: "slug",
|
|
73
|
+
description: "The URL path for this article",
|
|
74
|
+
options: {
|
|
75
|
+
source: "title",
|
|
76
|
+
maxLength: 96,
|
|
77
|
+
},
|
|
78
|
+
validation: (Rule) => Rule.required(),
|
|
79
|
+
}),
|
|
80
|
+
defineField({
|
|
81
|
+
name: "author",
|
|
82
|
+
title: "Author",
|
|
83
|
+
type: "string",
|
|
84
|
+
description: "The author of this article",
|
|
85
|
+
}),
|
|
86
|
+
defineField({
|
|
87
|
+
name: "publishedAt",
|
|
88
|
+
title: "Published At",
|
|
89
|
+
type: "datetime",
|
|
90
|
+
description: "When this article was published",
|
|
91
|
+
initialValue: () => new Date().toISOString(),
|
|
92
|
+
}),
|
|
93
|
+
defineField({
|
|
94
|
+
name: "metadata",
|
|
95
|
+
title: "SEO & Metadata",
|
|
96
|
+
type: "metadata",
|
|
97
|
+
description: "SEO settings for this article",
|
|
98
|
+
}),
|
|
99
|
+
...articleFields,
|
|
100
|
+
],
|
|
101
|
+
preview: {
|
|
102
|
+
select: {
|
|
103
|
+
title: "title",
|
|
104
|
+
slug: "slug.current",
|
|
105
|
+
media: "featuredImage",
|
|
106
|
+
},
|
|
107
|
+
prepare({ title, slug, media }) {
|
|
108
|
+
return {
|
|
109
|
+
title: title || "Untitled",
|
|
110
|
+
subtitle: slug ? `/blog/${slug}` : "No slug",
|
|
111
|
+
media,
|
|
112
|
+
};
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
orderings: [
|
|
116
|
+
{
|
|
117
|
+
title: "Published Date, New",
|
|
118
|
+
name: "publishedAtDesc",
|
|
119
|
+
by: [{ field: "publishedAt", direction: "desc" }],
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
title: "Published Date, Old",
|
|
123
|
+
name: "publishedAtAsc",
|
|
124
|
+
by: [{ field: "publishedAt", direction: "asc" }],
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
title: "Title A-Z",
|
|
128
|
+
name: "titleAsc",
|
|
129
|
+
by: [{ field: "title", direction: "asc" }],
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
});
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { defineField, defineType } from "sanity";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Example schema demonstrating common patterns and best practices
|
|
5
|
+
* This can be used as a reference for creating new content types
|
|
6
|
+
*/
|
|
7
|
+
export const example = defineType({
|
|
8
|
+
name: "example",
|
|
9
|
+
title: "Example Page",
|
|
10
|
+
type: "document",
|
|
11
|
+
fields: [
|
|
12
|
+
// Basic required fields
|
|
13
|
+
defineField({
|
|
14
|
+
name: "title",
|
|
15
|
+
title: "Title",
|
|
16
|
+
type: "string",
|
|
17
|
+
description: "The main title of the page",
|
|
18
|
+
validation: (Rule) => Rule.required().max(60),
|
|
19
|
+
}),
|
|
20
|
+
defineField({
|
|
21
|
+
name: "slug",
|
|
22
|
+
title: "Slug",
|
|
23
|
+
type: "slug",
|
|
24
|
+
description: "URL path for this page",
|
|
25
|
+
options: {
|
|
26
|
+
source: "title",
|
|
27
|
+
maxLength: 96,
|
|
28
|
+
},
|
|
29
|
+
validation: (Rule) => Rule.required(),
|
|
30
|
+
}),
|
|
31
|
+
|
|
32
|
+
// Hero section object
|
|
33
|
+
defineField({
|
|
34
|
+
name: "hero",
|
|
35
|
+
title: "Hero Section",
|
|
36
|
+
type: "object",
|
|
37
|
+
fields: [
|
|
38
|
+
{
|
|
39
|
+
name: "headline",
|
|
40
|
+
title: "Headline",
|
|
41
|
+
type: "string",
|
|
42
|
+
validation: (Rule) => Rule.max(100),
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "subheadline",
|
|
46
|
+
title: "Subheadline",
|
|
47
|
+
type: "text",
|
|
48
|
+
rows: 3,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "image",
|
|
52
|
+
title: "Hero Image",
|
|
53
|
+
type: "image",
|
|
54
|
+
options: {
|
|
55
|
+
hotspot: true,
|
|
56
|
+
},
|
|
57
|
+
fields: [
|
|
58
|
+
{
|
|
59
|
+
name: "alt",
|
|
60
|
+
title: "Alt Text",
|
|
61
|
+
type: "string",
|
|
62
|
+
description: "Alternative text for screen readers",
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "showCTA",
|
|
68
|
+
title: "Show Call to Action",
|
|
69
|
+
type: "boolean",
|
|
70
|
+
initialValue: false,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: "ctaText",
|
|
74
|
+
title: "CTA Text",
|
|
75
|
+
type: "string",
|
|
76
|
+
hidden: ({ parent }) => !parent?.showCTA,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "ctaLink",
|
|
80
|
+
title: "CTA Link",
|
|
81
|
+
type: "url",
|
|
82
|
+
hidden: ({ parent }) => !parent?.showCTA,
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
}),
|
|
86
|
+
|
|
87
|
+
// Rich text content
|
|
88
|
+
defineField({
|
|
89
|
+
name: "content",
|
|
90
|
+
title: "Content",
|
|
91
|
+
type: "richText",
|
|
92
|
+
description: "The main content of the page",
|
|
93
|
+
}),
|
|
94
|
+
|
|
95
|
+
// Features array
|
|
96
|
+
defineField({
|
|
97
|
+
name: "features",
|
|
98
|
+
title: "Features",
|
|
99
|
+
type: "array",
|
|
100
|
+
of: [
|
|
101
|
+
{
|
|
102
|
+
type: "object",
|
|
103
|
+
fields: [
|
|
104
|
+
{
|
|
105
|
+
name: "title",
|
|
106
|
+
title: "Feature Title",
|
|
107
|
+
type: "string",
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: "description",
|
|
111
|
+
title: "Description",
|
|
112
|
+
type: "text",
|
|
113
|
+
rows: 3,
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: "icon",
|
|
117
|
+
title: "Icon",
|
|
118
|
+
type: "image",
|
|
119
|
+
options: {
|
|
120
|
+
hotspot: true,
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
}),
|
|
127
|
+
|
|
128
|
+
// Tags array
|
|
129
|
+
defineField({
|
|
130
|
+
name: "tags",
|
|
131
|
+
title: "Tags",
|
|
132
|
+
type: "array",
|
|
133
|
+
of: [{ type: "string" }],
|
|
134
|
+
options: {
|
|
135
|
+
layout: "tags",
|
|
136
|
+
},
|
|
137
|
+
}),
|
|
138
|
+
|
|
139
|
+
// SEO metadata
|
|
140
|
+
defineField({
|
|
141
|
+
name: "metadata",
|
|
142
|
+
title: "SEO & Metadata",
|
|
143
|
+
type: "metadata",
|
|
144
|
+
description: "SEO settings for this page",
|
|
145
|
+
}),
|
|
146
|
+
|
|
147
|
+
// Publishing info
|
|
148
|
+
defineField({
|
|
149
|
+
name: "publishedAt",
|
|
150
|
+
title: "Published At",
|
|
151
|
+
type: "datetime",
|
|
152
|
+
description: "When this page was published",
|
|
153
|
+
initialValue: () => new Date().toISOString(),
|
|
154
|
+
}),
|
|
155
|
+
|
|
156
|
+
// Conditional field example
|
|
157
|
+
defineField({
|
|
158
|
+
name: "showInNavigation",
|
|
159
|
+
title: "Show in Navigation",
|
|
160
|
+
type: "boolean",
|
|
161
|
+
initialValue: false,
|
|
162
|
+
}),
|
|
163
|
+
|
|
164
|
+
// Custom validation example
|
|
165
|
+
defineField({
|
|
166
|
+
name: "contactEmail",
|
|
167
|
+
title: "Contact Email",
|
|
168
|
+
type: "string",
|
|
169
|
+
validation: (Rule) =>
|
|
170
|
+
Rule.email().error("Please enter a valid email address"),
|
|
171
|
+
}),
|
|
172
|
+
],
|
|
173
|
+
|
|
174
|
+
// Custom preview
|
|
175
|
+
preview: {
|
|
176
|
+
select: {
|
|
177
|
+
title: "title",
|
|
178
|
+
slug: "slug.current",
|
|
179
|
+
media: "hero.image",
|
|
180
|
+
},
|
|
181
|
+
prepare({ title, slug, media }) {
|
|
182
|
+
return {
|
|
183
|
+
title: title || "Untitled Example",
|
|
184
|
+
subtitle: slug ? `/${slug}` : "No slug",
|
|
185
|
+
media,
|
|
186
|
+
};
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
// Ordering options
|
|
191
|
+
orderings: [
|
|
192
|
+
{
|
|
193
|
+
title: "Published Date, New",
|
|
194
|
+
name: "publishedAtDesc",
|
|
195
|
+
by: [{ field: "publishedAt", direction: "desc" }],
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
title: "Title A-Z",
|
|
199
|
+
name: "titleAsc",
|
|
200
|
+
by: [{ field: "title", direction: "asc" }],
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sanity Schema Types
|
|
3
|
+
*
|
|
4
|
+
* All schema definitions for Sanity CMS, organized in a flat structure.
|
|
5
|
+
* This replaces the previous documents/objects/singletons directory structure.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { SchemaTypeDefinition } from "sanity";
|
|
9
|
+
import { article } from "./article";
|
|
10
|
+
import { example } from "./example";
|
|
11
|
+
// Import all schema definitions
|
|
12
|
+
import { link } from "./link";
|
|
13
|
+
import { metadata } from "./metadata";
|
|
14
|
+
import { navigation } from "./navigation";
|
|
15
|
+
import { page } from "./page";
|
|
16
|
+
import { richText } from "./richText";
|
|
17
|
+
|
|
18
|
+
// Re-export all schemas for convenience
|
|
19
|
+
export { link, metadata, richText, article, example, page, navigation };
|
|
20
|
+
|
|
21
|
+
// Schema collection for Sanity configuration
|
|
22
|
+
export const schema: { types: SchemaTypeDefinition[] } = {
|
|
23
|
+
types: [
|
|
24
|
+
// Object types (reusable components)
|
|
25
|
+
link,
|
|
26
|
+
metadata,
|
|
27
|
+
richText,
|
|
28
|
+
|
|
29
|
+
// Document types (content pages)
|
|
30
|
+
page,
|
|
31
|
+
article,
|
|
32
|
+
example,
|
|
33
|
+
|
|
34
|
+
// Singleton types (one-off content)
|
|
35
|
+
navigation,
|
|
36
|
+
],
|
|
37
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { defineField, defineType } from "sanity";
|
|
2
|
+
|
|
3
|
+
// Native Sanity link object type
|
|
4
|
+
export const link = defineType({
|
|
5
|
+
name: "link",
|
|
6
|
+
title: "Link",
|
|
7
|
+
type: "object",
|
|
8
|
+
fields: [
|
|
9
|
+
defineField({
|
|
10
|
+
name: "linkType",
|
|
11
|
+
title: "Link Type",
|
|
12
|
+
type: "string",
|
|
13
|
+
options: {
|
|
14
|
+
list: [
|
|
15
|
+
{ title: "Internal", value: "internal" },
|
|
16
|
+
{ title: "External", value: "external" },
|
|
17
|
+
],
|
|
18
|
+
layout: "radio",
|
|
19
|
+
},
|
|
20
|
+
initialValue: "internal",
|
|
21
|
+
validation: (Rule) => Rule.required(),
|
|
22
|
+
}),
|
|
23
|
+
defineField({
|
|
24
|
+
name: "internalLink",
|
|
25
|
+
title: "Internal Link",
|
|
26
|
+
type: "reference",
|
|
27
|
+
to: [{ type: "page" }, { type: "article" }],
|
|
28
|
+
hidden: ({ parent }) => parent?.linkType !== "internal",
|
|
29
|
+
validation: (Rule) =>
|
|
30
|
+
Rule.custom((value, context) => {
|
|
31
|
+
const parent = context.parent as { linkType?: string };
|
|
32
|
+
if (parent?.linkType === "internal" && !value) {
|
|
33
|
+
return "Internal link is required";
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}),
|
|
37
|
+
}),
|
|
38
|
+
defineField({
|
|
39
|
+
name: "externalUrl",
|
|
40
|
+
title: "External URL",
|
|
41
|
+
type: "url",
|
|
42
|
+
hidden: ({ parent }) => parent?.linkType !== "external",
|
|
43
|
+
validation: (Rule) =>
|
|
44
|
+
Rule.custom((value, context) => {
|
|
45
|
+
const parent = context.parent as { linkType?: string };
|
|
46
|
+
if (parent?.linkType === "external" && !value) {
|
|
47
|
+
return "External URL is required";
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
}).uri({
|
|
51
|
+
scheme: ["http", "https", "mailto", "tel"],
|
|
52
|
+
}),
|
|
53
|
+
}),
|
|
54
|
+
defineField({
|
|
55
|
+
name: "text",
|
|
56
|
+
title: "Link Text",
|
|
57
|
+
type: "string",
|
|
58
|
+
description: "Optional custom text for the link",
|
|
59
|
+
}),
|
|
60
|
+
defineField({
|
|
61
|
+
name: "openInNewTab",
|
|
62
|
+
title: "Open in New Tab",
|
|
63
|
+
type: "boolean",
|
|
64
|
+
initialValue: false,
|
|
65
|
+
hidden: ({ parent }) => parent?.linkType !== "external",
|
|
66
|
+
}),
|
|
67
|
+
],
|
|
68
|
+
preview: {
|
|
69
|
+
select: {
|
|
70
|
+
linkType: "linkType",
|
|
71
|
+
text: "text",
|
|
72
|
+
internalTitle: "internalLink.title",
|
|
73
|
+
externalUrl: "externalUrl",
|
|
74
|
+
},
|
|
75
|
+
prepare({ linkType, text, internalTitle, externalUrl }) {
|
|
76
|
+
const title = text || internalTitle || externalUrl || "Untitled Link";
|
|
77
|
+
const subtitle =
|
|
78
|
+
linkType === "internal"
|
|
79
|
+
? `Internal: ${internalTitle || "No page selected"}`
|
|
80
|
+
: `External: ${externalUrl || "No URL"}`;
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
title,
|
|
84
|
+
subtitle,
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Helper field exports for easy reuse
|
|
91
|
+
export const linkField = defineField({
|
|
92
|
+
name: "link",
|
|
93
|
+
title: "Link",
|
|
94
|
+
type: "link",
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
export const linkFieldWithLabel = defineField({
|
|
98
|
+
name: "link",
|
|
99
|
+
title: "Link",
|
|
100
|
+
type: "link",
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
export const linkFieldWithLabelAndRequired = defineField({
|
|
104
|
+
name: "link",
|
|
105
|
+
title: "Link",
|
|
106
|
+
type: "link",
|
|
107
|
+
validation: (Rule) => Rule.required(),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
type LinkFieldOptions = {
|
|
111
|
+
name?: string;
|
|
112
|
+
title?: string;
|
|
113
|
+
required?: boolean;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export function extendedLinkField({
|
|
117
|
+
name = "link",
|
|
118
|
+
title = "Link",
|
|
119
|
+
required = false,
|
|
120
|
+
}: LinkFieldOptions = {}) {
|
|
121
|
+
return defineField({
|
|
122
|
+
name,
|
|
123
|
+
title,
|
|
124
|
+
type: "link",
|
|
125
|
+
validation: required ? (Rule) => Rule.required() : undefined,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { defineField, defineType } from "sanity";
|
|
2
|
+
|
|
3
|
+
export const metadata = defineType({
|
|
4
|
+
name: "metadata",
|
|
5
|
+
title: "SEO & Metadata",
|
|
6
|
+
type: "object",
|
|
7
|
+
options: {
|
|
8
|
+
collapsible: true,
|
|
9
|
+
collapsed: true,
|
|
10
|
+
},
|
|
11
|
+
fields: [
|
|
12
|
+
defineField({
|
|
13
|
+
name: "title",
|
|
14
|
+
title: "Meta Title",
|
|
15
|
+
type: "string",
|
|
16
|
+
description: "Title for search engines and social media",
|
|
17
|
+
validation: (Rule) =>
|
|
18
|
+
Rule.max(60).warning(
|
|
19
|
+
"Title should be under 60 characters for optimal SEO",
|
|
20
|
+
),
|
|
21
|
+
}),
|
|
22
|
+
defineField({
|
|
23
|
+
name: "description",
|
|
24
|
+
title: "Meta Description",
|
|
25
|
+
type: "text",
|
|
26
|
+
rows: 3,
|
|
27
|
+
description: "Description for search engines and social media",
|
|
28
|
+
validation: (Rule) =>
|
|
29
|
+
Rule.max(160).warning(
|
|
30
|
+
"Description should be under 160 characters for optimal SEO",
|
|
31
|
+
),
|
|
32
|
+
}),
|
|
33
|
+
defineField({
|
|
34
|
+
name: "keywords",
|
|
35
|
+
title: "Keywords",
|
|
36
|
+
type: "array",
|
|
37
|
+
of: [{ type: "string" }],
|
|
38
|
+
description: "Keywords for search engines",
|
|
39
|
+
options: {
|
|
40
|
+
layout: "tags",
|
|
41
|
+
},
|
|
42
|
+
}),
|
|
43
|
+
defineField({
|
|
44
|
+
name: "image",
|
|
45
|
+
title: "Social Media Image",
|
|
46
|
+
type: "image",
|
|
47
|
+
description: "Image for social media sharing (1200x630px recommended)",
|
|
48
|
+
options: {
|
|
49
|
+
hotspot: true,
|
|
50
|
+
},
|
|
51
|
+
fields: [
|
|
52
|
+
{
|
|
53
|
+
name: "alt",
|
|
54
|
+
title: "Alt Text",
|
|
55
|
+
type: "string",
|
|
56
|
+
description: "Alternative text for screen readers",
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
}),
|
|
60
|
+
defineField({
|
|
61
|
+
name: "noIndex",
|
|
62
|
+
title: "No Index",
|
|
63
|
+
type: "boolean",
|
|
64
|
+
description: "Prevent search engines from indexing this page",
|
|
65
|
+
initialValue: false,
|
|
66
|
+
}),
|
|
67
|
+
],
|
|
68
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { defineField, defineType } from "sanity";
|
|
2
|
+
|
|
3
|
+
export const navigation = defineType({
|
|
4
|
+
name: "navigation",
|
|
5
|
+
title: "Navigation",
|
|
6
|
+
type: "document",
|
|
7
|
+
fields: [
|
|
8
|
+
defineField({
|
|
9
|
+
name: "title",
|
|
10
|
+
type: "string",
|
|
11
|
+
title: "Title",
|
|
12
|
+
description: "Title of the navigation",
|
|
13
|
+
}),
|
|
14
|
+
defineField({
|
|
15
|
+
type: "array",
|
|
16
|
+
name: "socials",
|
|
17
|
+
title: "Socials",
|
|
18
|
+
of: [
|
|
19
|
+
{
|
|
20
|
+
name: "socialLink",
|
|
21
|
+
type: "object",
|
|
22
|
+
title: "Social Link",
|
|
23
|
+
fields: [
|
|
24
|
+
defineField({
|
|
25
|
+
name: "logo",
|
|
26
|
+
type: "image",
|
|
27
|
+
title: "Logo",
|
|
28
|
+
}),
|
|
29
|
+
defineField({
|
|
30
|
+
name: "socialMedia",
|
|
31
|
+
title: "Social Media",
|
|
32
|
+
type: "link",
|
|
33
|
+
}),
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
}),
|
|
38
|
+
],
|
|
39
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { defineField, defineType } from "sanity";
|
|
2
|
+
import { linkFieldWithLabelAndRequired } from "./link";
|
|
3
|
+
|
|
4
|
+
const pageFields = [
|
|
5
|
+
defineField({
|
|
6
|
+
name: "title",
|
|
7
|
+
title: "Title",
|
|
8
|
+
type: "string",
|
|
9
|
+
description: "The title of the page",
|
|
10
|
+
validation: (Rule) => Rule.required(),
|
|
11
|
+
}),
|
|
12
|
+
defineField({
|
|
13
|
+
name: "content",
|
|
14
|
+
title: "Content",
|
|
15
|
+
type: "richText",
|
|
16
|
+
description: "The main content of the page",
|
|
17
|
+
}),
|
|
18
|
+
linkFieldWithLabelAndRequired,
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export const page = defineType({
|
|
22
|
+
name: "page",
|
|
23
|
+
title: "Page",
|
|
24
|
+
type: "document",
|
|
25
|
+
fields: [
|
|
26
|
+
defineField({
|
|
27
|
+
name: "slug",
|
|
28
|
+
title: "Slug",
|
|
29
|
+
type: "slug",
|
|
30
|
+
description: "The URL path for this page",
|
|
31
|
+
options: {
|
|
32
|
+
source: "title",
|
|
33
|
+
maxLength: 96,
|
|
34
|
+
},
|
|
35
|
+
validation: (Rule) => Rule.required(),
|
|
36
|
+
}),
|
|
37
|
+
defineField({
|
|
38
|
+
name: "publishedAt",
|
|
39
|
+
title: "Published At",
|
|
40
|
+
type: "datetime",
|
|
41
|
+
description: "When this page was published",
|
|
42
|
+
initialValue: () => new Date().toISOString(),
|
|
43
|
+
}),
|
|
44
|
+
|
|
45
|
+
defineField({
|
|
46
|
+
name: "metadata",
|
|
47
|
+
title: "SEO & Metadata",
|
|
48
|
+
type: "metadata",
|
|
49
|
+
description: "SEO settings for this page",
|
|
50
|
+
}),
|
|
51
|
+
...pageFields,
|
|
52
|
+
],
|
|
53
|
+
preview: {
|
|
54
|
+
select: {
|
|
55
|
+
title: "title",
|
|
56
|
+
slug: "slug.current",
|
|
57
|
+
},
|
|
58
|
+
prepare({ title, slug }) {
|
|
59
|
+
return {
|
|
60
|
+
title: title || "Untitled",
|
|
61
|
+
subtitle: slug ? `/${slug}` : "No slug",
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
orderings: [
|
|
66
|
+
{
|
|
67
|
+
title: "Published Date, New",
|
|
68
|
+
name: "publishedAtDesc",
|
|
69
|
+
by: [{ field: "publishedAt", direction: "desc" }],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
title: "Published Date, Old",
|
|
73
|
+
name: "publishedAtAsc",
|
|
74
|
+
by: [{ field: "publishedAt", direction: "asc" }],
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
});
|