bsmnt 0.4.2 → 0.5.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.
Files changed (188) hide show
  1. package/dist/application/add-integration/index.d.ts +24 -6
  2. package/dist/application/add-integration/index.d.ts.map +1 -1
  3. package/dist/application/add-integration/index.js +130 -82
  4. package/dist/application/add-integration/index.js.map +1 -1
  5. package/dist/application/create-project/map-selection-to-registry.js +1 -1
  6. package/dist/application/create-project/map-selection-to-registry.js.map +1 -1
  7. package/dist/application/shared/project-detection.d.ts +2 -2
  8. package/dist/application/shared/project-detection.d.ts.map +1 -1
  9. package/dist/application/shared/project-detection.js +2 -2
  10. package/dist/application/shared/project-detection.js.map +1 -1
  11. package/dist/core/create/internal-ids.d.ts +4 -1
  12. package/dist/core/create/internal-ids.d.ts.map +1 -1
  13. package/dist/core/create/internal-ids.js +6 -1
  14. package/dist/core/create/internal-ids.js.map +1 -1
  15. package/dist/domain/cms.d.ts +0 -7
  16. package/dist/domain/cms.d.ts.map +1 -1
  17. package/dist/domain/cms.js.map +1 -1
  18. package/dist/domain/forms.d.ts +17 -0
  19. package/dist/domain/forms.d.ts.map +1 -0
  20. package/dist/domain/forms.js +13 -0
  21. package/dist/domain/forms.js.map +1 -0
  22. package/dist/domain/integration.d.ts +30 -0
  23. package/dist/domain/integration.d.ts.map +1 -0
  24. package/dist/domain/integration.js +12 -0
  25. package/dist/domain/integration.js.map +1 -0
  26. package/dist/domain/skills.d.ts +32 -0
  27. package/dist/domain/skills.d.ts.map +1 -1
  28. package/dist/domain/skills.js +39 -0
  29. package/dist/domain/skills.js.map +1 -1
  30. package/dist/index.js +95 -37
  31. package/dist/index.js.map +1 -1
  32. package/dist/infrastructure/agents/register-sanity-mcp.js +9 -6
  33. package/dist/infrastructure/agents/register-sanity-mcp.js.map +1 -1
  34. package/dist/infrastructure/create/apply-package-json.d.ts.map +1 -1
  35. package/dist/infrastructure/create/apply-package-json.js +19 -0
  36. package/dist/infrastructure/create/apply-package-json.js.map +1 -1
  37. package/dist/infrastructure/create/install-dependencies.d.ts.map +1 -1
  38. package/dist/infrastructure/create/install-dependencies.js +4 -0
  39. package/dist/infrastructure/create/install-dependencies.js.map +1 -1
  40. package/dist/infrastructure/create/merge-registry.d.ts.map +1 -1
  41. package/dist/infrastructure/create/merge-registry.js +8 -0
  42. package/dist/infrastructure/create/merge-registry.js.map +1 -1
  43. package/dist/infrastructure/create/setup-agent.d.ts.map +1 -1
  44. package/dist/infrastructure/create/setup-agent.js +29 -6
  45. package/dist/infrastructure/create/setup-agent.js.map +1 -1
  46. package/dist/infrastructure/create/setup-sanity.d.ts.map +1 -1
  47. package/dist/infrastructure/create/setup-sanity.js +3 -0
  48. package/dist/infrastructure/create/setup-sanity.js.map +1 -1
  49. package/dist/infrastructure/integrations/inject-integration.d.ts +3 -3
  50. package/dist/infrastructure/integrations/inject-integration.d.ts.map +1 -1
  51. package/dist/infrastructure/integrations/inject-integration.js +81 -37
  52. package/dist/infrastructure/integrations/inject-integration.js.map +1 -1
  53. package/dist/modules/features/cms/sanity/config.d.ts.map +1 -1
  54. package/dist/modules/features/cms/sanity/config.js +11 -5
  55. package/dist/modules/features/cms/sanity/config.js.map +1 -1
  56. package/dist/modules/features/cms/sanity/index.js +8 -2
  57. package/dist/modules/features/cms/sanity/index.js.map +1 -1
  58. package/dist/modules/features/cms/sanity/mergers/ci-workflow.d.ts +13 -0
  59. package/dist/modules/features/cms/sanity/mergers/ci-workflow.d.ts.map +1 -0
  60. package/dist/modules/features/cms/sanity/mergers/ci-workflow.js +38 -0
  61. package/dist/modules/features/cms/sanity/mergers/ci-workflow.js.map +1 -0
  62. package/dist/modules/features/cms/sanity/mergers/knip.d.ts +12 -0
  63. package/dist/modules/features/cms/sanity/mergers/knip.d.ts.map +1 -0
  64. package/dist/modules/features/cms/sanity/mergers/knip.js +38 -0
  65. package/dist/modules/features/cms/sanity/mergers/knip.js.map +1 -0
  66. package/dist/modules/features/cms/sanity/mergers/layout.d.ts.map +1 -1
  67. package/dist/modules/features/cms/sanity/mergers/layout.js +7 -3
  68. package/dist/modules/features/cms/sanity/mergers/layout.js.map +1 -1
  69. package/dist/modules/features/cms/sanity/mergers/sitemap.d.ts +3 -3
  70. package/dist/modules/features/cms/sanity/mergers/sitemap.d.ts.map +1 -1
  71. package/dist/modules/features/cms/sanity/mergers/sitemap.js +48 -62
  72. package/dist/modules/features/cms/sanity/mergers/sitemap.js.map +1 -1
  73. package/dist/modules/features/cms/sanity-pagebuilder/config.d.ts.map +1 -1
  74. package/dist/modules/features/cms/sanity-pagebuilder/config.js +8 -5
  75. package/dist/modules/features/cms/sanity-pagebuilder/config.js.map +1 -1
  76. package/dist/modules/features/cms/sanity-pagebuilder/index.js +2 -2
  77. package/dist/modules/features/cms/sanity-pagebuilder/index.js.map +1 -1
  78. package/dist/modules/features/forms/hubspot/config.d.ts +31 -0
  79. package/dist/modules/features/forms/hubspot/config.d.ts.map +1 -0
  80. package/dist/modules/features/forms/hubspot/config.js +25 -0
  81. package/dist/modules/features/forms/hubspot/config.js.map +1 -0
  82. package/dist/paths.d.ts +8 -0
  83. package/dist/paths.d.ts.map +1 -1
  84. package/dist/paths.js +8 -0
  85. package/dist/paths.js.map +1 -1
  86. package/index.js +2 -2
  87. package/package.json +17 -9
  88. package/src/assets/check-node-version.mjs +22 -0
  89. package/src/modules/features/cms/sanity/files/app/blog/page.tsx +54 -0
  90. package/src/modules/features/cms/sanity/files/app/feed.xml/route.ts +36 -0
  91. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/components/disable-draft-mode.tsx +3 -3
  92. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/schemas/article.ts +7 -7
  93. package/src/modules/features/cms/sanity/files/lib/scripts/setup-ci-secrets.ts +117 -0
  94. package/src/modules/features/cms/sanity/files/lib/utils/rss.ts +96 -0
  95. package/src/modules/features/cms/sanity-pagebuilder/files/.knip.json +8 -0
  96. package/src/modules/features/cms/sanity-pagebuilder/files/app/api/[[...slug]]/route.ts +100 -0
  97. package/src/modules/features/cms/sanity-pagebuilder/files/app/api/draft-mode/disable/route.ts +7 -0
  98. package/src/modules/features/cms/sanity-pagebuilder/files/app/api/draft-mode/enable/route.ts +20 -0
  99. package/src/modules/features/cms/sanity-pagebuilder/files/app/api/revalidate/route.ts +126 -0
  100. package/src/modules/features/cms/sanity-pagebuilder/files/app/feed.xml/route.ts +40 -0
  101. package/src/modules/features/cms/sanity-pagebuilder/files/app/sitemap.md/route.ts +124 -0
  102. package/src/modules/features/cms/sanity-pagebuilder/files/app/studio/[[...tool]]/page.tsx +8 -0
  103. package/src/modules/features/cms/sanity-pagebuilder/files/components/sanity/draft-mode-toggle.tsx +27 -0
  104. package/src/modules/features/cms/sanity-pagebuilder/files/components/sanity/rich-text.tsx +87 -0
  105. package/src/modules/features/cms/sanity-pagebuilder/files/components/sanity/visual-editing.tsx +28 -0
  106. package/src/modules/features/cms/sanity-pagebuilder/files/components/ui/sanity-image/index.tsx +41 -0
  107. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/check-integration.ts +5 -0
  108. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/client.ts +27 -0
  109. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/disable-draft-mode.tsx +23 -0
  110. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/page-builder-input.tsx +39 -0
  111. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/page-category-input.tsx +50 -0
  112. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/slug-field.tsx +18 -0
  113. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/slug-input.tsx +14 -0
  114. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/confirm-publish-action.ts +40 -0
  115. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/env.ts +34 -0
  116. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/fetchers/layout.ts +48 -0
  117. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/fetchers/redirects.ts +17 -0
  118. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/icons.ts +62 -0
  119. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/live/index.tsx +88 -0
  120. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/markdown-proxy.config.ts +50 -0
  121. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/page-builder-config.ts +130 -0
  122. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/page-category.ts +28 -0
  123. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/page-tree-pane.tsx +420 -0
  124. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/pinned-pages.ts +54 -0
  125. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/presentation.ts +119 -0
  126. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/queries.ts +385 -0
  127. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/sanity.cli.ts +29 -0
  128. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/sanity.config.ts +113 -0
  129. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/index.ts +4 -0
  130. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/blog-content.ts +89 -0
  131. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/description.ts +29 -0
  132. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/reusable/hero.ts +28 -0
  133. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/singleton/content-collection.ts +45 -0
  134. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/content/author.ts +70 -0
  135. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/content/blog-category.ts +55 -0
  136. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/index.ts +96 -0
  137. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/company-data.ts +62 -0
  138. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/footer.ts +79 -0
  139. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/navbar.ts +75 -0
  140. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/redirect.ts +120 -0
  141. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/link.ts +125 -0
  142. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/logo-field.ts +9 -0
  143. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/metadata.ts +68 -0
  144. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/nav-objects.ts +192 -0
  145. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/page-builder.ts +39 -0
  146. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/page-folder.ts +124 -0
  147. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/page.ts +242 -0
  148. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/richText.ts +63 -0
  149. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/singletons.ts +39 -0
  150. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/structure.ts +330 -0
  151. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/utils/image.ts +8 -0
  152. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/utils/link.ts +140 -0
  153. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/utils/page-builder-markdown.ts +81 -0
  154. package/src/modules/features/cms/sanity-pagebuilder/files/lib/scripts/sanity-typegen.ts +45 -0
  155. package/src/modules/features/cms/sanity-pagebuilder/files/lib/utils/check-redirect.ts +30 -0
  156. package/src/modules/features/cms/sanity-pagebuilder/files/lib/utils/json-ld.tsx +212 -0
  157. package/src/modules/features/cms/sanity-pagebuilder/files/lib/utils/metadata.ts +167 -0
  158. package/src/modules/features/cms/sanity-pagebuilder/files/lib/utils/rss.ts +96 -0
  159. package/src/modules/features/cms/sanity-pagebuilder/files/lib/utils/sitemap.ts +40 -0
  160. package/src/modules/features/cms/sanity-pagebuilder/files/proxy.ts +81 -0
  161. package/src/templates/next-default/.github/workflows/ci.yml +62 -0
  162. package/src/templates/next-default/README.md +22 -0
  163. package/src/templates/next-default/knip.json +14 -0
  164. package/src/templates/next-default/lefthook.yml +5 -0
  165. package/src/templates/next-default/package.json +8 -4
  166. package/src/templates/next-experiments/.github/workflows/ci.yml +62 -0
  167. package/src/templates/next-experiments/README.md +22 -0
  168. package/src/templates/next-experiments/knip.json +14 -0
  169. package/src/templates/next-experiments/lefthook.yml +30 -0
  170. package/src/templates/next-experiments/package.json +10 -2
  171. package/src/templates/next-pagebuilder/.github/workflows/ci.yml +70 -0
  172. package/src/templates/next-pagebuilder/README.md +34 -0
  173. package/src/templates/next-pagebuilder/app/(content)/[[...slug]]/page.tsx +6 -1
  174. package/src/templates/next-pagebuilder/app/(content)/layout.tsx +3 -12
  175. package/src/templates/next-pagebuilder/components/layout/footer/index.tsx +11 -10
  176. package/src/templates/next-pagebuilder/components/layout/header/index.tsx +5 -3
  177. package/src/templates/next-pagebuilder/components/layout/json-ld/index.tsx +9 -10
  178. package/src/templates/next-pagebuilder/components/layout/wrapper/index.tsx +3 -11
  179. package/src/templates/next-pagebuilder/components/page-builder/components/content-collection/index.tsx +1 -1
  180. package/src/templates/next-pagebuilder/knip.json +27 -0
  181. package/src/templates/next-pagebuilder/lefthook.yml +5 -0
  182. package/src/templates/next-pagebuilder/lib/scripts/setup-ci-secrets.ts +117 -0
  183. package/src/templates/next-pagebuilder/package.json +9 -3
  184. package/src/templates/next-webgl/.github/workflows/ci.yml +62 -0
  185. package/src/templates/next-webgl/README.md +22 -0
  186. package/src/templates/next-webgl/knip.json +13 -0
  187. package/src/templates/next-webgl/lefthook.yml +5 -0
  188. package/src/templates/next-webgl/package.json +8 -4
@@ -0,0 +1,124 @@
1
+ import { NextResponse } from "next/server";
2
+ import { isSanityConfigured } from "@/lib/integrations/check-integration";
3
+ import type { PAGE_SITEMAP_QUERY_RESULT } from "@/lib/integrations/sanity/sanity.types";
4
+ import {
5
+ getMarkdownPath,
6
+ getSitemap,
7
+ getSitemapBaseUrl,
8
+ } from "@/lib/utils/sitemap";
9
+
10
+ export const dynamic = "force-dynamic";
11
+
12
+ type PageDoc = PAGE_SITEMAP_QUERY_RESULT[number];
13
+
14
+ const ROOT_GROUP = "__root__";
15
+
16
+ const getFolderGroup = (slug: string | null) => {
17
+ if (!slug) return ROOT_GROUP;
18
+
19
+ const segments = slug.split("/").filter(Boolean);
20
+
21
+ if (segments.length <= 1) return ROOT_GROUP;
22
+
23
+ return segments.slice(0, -1).join("/");
24
+ };
25
+
26
+ const getGroupHeading = (group: string) => {
27
+ if (group === ROOT_GROUP) return "### Root Pages";
28
+
29
+ return `### /${group}`;
30
+ };
31
+
32
+ const sortPages = (left: PageDoc, right: PageDoc) => {
33
+ const leftLabel = left.title || left.slug || "Homepage";
34
+ const rightLabel = right.title || right.slug || "Homepage";
35
+
36
+ return leftLabel.localeCompare(rightLabel);
37
+ };
38
+
39
+ export const GET = async () => {
40
+ const baseUrl = getSitemapBaseUrl();
41
+
42
+ if (!isSanityConfigured()) {
43
+ return new NextResponse(
44
+ [
45
+ "# Sitemap",
46
+ "",
47
+ "## Pages",
48
+ "",
49
+ `- [Homepage](${baseUrl}/index.md)`,
50
+ "",
51
+ "CMS not configured. No additional content available.",
52
+ ].join("\n"),
53
+ {
54
+ headers: { "Content-Type": "text/markdown; charset=utf-8" },
55
+ },
56
+ );
57
+ }
58
+
59
+ try {
60
+ const allPages = await getSitemap();
61
+
62
+ if (!allPages) {
63
+ return new NextResponse("# Sitemap\n\nUnable to connect to CMS.", {
64
+ status: 503,
65
+ headers: { "Content-Type": "text/markdown; charset=utf-8" },
66
+ });
67
+ }
68
+
69
+ const pages = (allPages ?? []).filter((page) => !page.noIndex);
70
+ const pageGroups = new Map<string, PageDoc[]>();
71
+
72
+ for (const page of pages) {
73
+ const group = getFolderGroup(page.slug ?? null);
74
+ const groupedPages = pageGroups.get(group) ?? [];
75
+
76
+ groupedPages.push(page);
77
+ pageGroups.set(group, groupedPages);
78
+ }
79
+
80
+ const sortedGroupNames = [...pageGroups.keys()].sort((left, right) => {
81
+ if (left === ROOT_GROUP) return -1;
82
+ if (right === ROOT_GROUP) return 1;
83
+
84
+ return left.localeCompare(right);
85
+ });
86
+
87
+ const parts = [
88
+ `# ${new URL(baseUrl).hostname} - Content Sitemap`,
89
+ "",
90
+ "> This sitemap lists all available markdown content for AI agents.",
91
+ "",
92
+ "## Pages",
93
+ "",
94
+ ];
95
+
96
+ if (pages.length > 0) {
97
+ for (const groupName of sortedGroupNames) {
98
+ const groupedPages = (pageGroups.get(groupName) ?? []).sort(sortPages);
99
+
100
+ if (groupedPages.length === 0) continue;
101
+
102
+ parts.push(getGroupHeading(groupName), "");
103
+
104
+ for (const page of groupedPages) {
105
+ parts.push(
106
+ `- [${page.title || page.slug || "Homepage"}](${getMarkdownPath(baseUrl, page.slug ?? null)})`,
107
+ );
108
+ }
109
+
110
+ parts.push("");
111
+ }
112
+ }
113
+
114
+ if (pages.length === 0) parts.push("No content published yet.", "");
115
+
116
+ return new NextResponse(parts.join("\n"));
117
+ } catch (error) {
118
+ console.error("Error generating sitemap.md:", error);
119
+ return new NextResponse("# Sitemap\n\nError fetching content.", {
120
+ status: 500,
121
+ headers: { "Content-Type": "text/markdown; charset=utf-8" },
122
+ });
123
+ }
124
+ };
@@ -0,0 +1,8 @@
1
+ "use client";
2
+
3
+ import { NextStudio } from "next-sanity/studio";
4
+ import config from "@/lib/integrations/sanity/sanity.config";
5
+
6
+ const StudioPage = () => <NextStudio config={config} />;
7
+
8
+ export default StudioPage;
@@ -0,0 +1,27 @@
1
+ "use client";
2
+
3
+ import { useRouter } from "next/navigation";
4
+ import { useTransition } from "react";
5
+
6
+ export function DraftModeToggle() {
7
+ const router = useRouter();
8
+ const [isPending, startTransition] = useTransition();
9
+
10
+ const handleDisable = () => {
11
+ startTransition(async () => {
12
+ await fetch("/api/draft-mode/disable");
13
+ router.refresh();
14
+ });
15
+ };
16
+
17
+ return (
18
+ <button
19
+ type="button"
20
+ onClick={handleDisable}
21
+ disabled={isPending}
22
+ className="fixed bottom-4 left-1/2 z-50 -translate-x-1/2 rounded-full border border-white/10 bg-black/80 px-2 py-1 font-medium text-sm text-white shadow-lg backdrop-blur-sm transition-colors hover:bg-black/90 disabled:opacity-50"
23
+ >
24
+ {isPending ? "Disabling..." : "Disable Draft Mode"}
25
+ </button>
26
+ );
27
+ }
@@ -0,0 +1,87 @@
1
+ import { PortableText, type PortableTextProps } from "@portabletext/react";
2
+ import { Link } from "@/components/ui/link";
3
+ import { SanityImage } from "@/components/ui/sanity-image";
4
+ import {
5
+ getLinkAttributes,
6
+ urlForReference,
7
+ } from "../../lib/integrations/sanity/utils/link";
8
+
9
+ interface RichTextProps {
10
+ content: PortableTextProps["value"];
11
+ }
12
+
13
+ type LinkMarkProps = {
14
+ children: React.ReactNode;
15
+ value?: unknown;
16
+ };
17
+
18
+ const LinkMark = ({ children, value }: LinkMarkProps) => {
19
+ const href = urlForReference(value as Parameters<typeof urlForReference>[0]);
20
+ const attrs = getLinkAttributes(
21
+ value as Parameters<typeof getLinkAttributes>[0],
22
+ );
23
+
24
+ return (
25
+ <Link
26
+ href={href}
27
+ target={attrs.target}
28
+ rel={attrs.rel}
29
+ data-sanity-edit-target
30
+ >
31
+ {children}
32
+ </Link>
33
+ );
34
+ };
35
+
36
+ export const RichText = ({ content }: RichTextProps) => {
37
+ if (!content) return null;
38
+
39
+ return (
40
+ <PortableText
41
+ value={content}
42
+ components={{
43
+ types: {
44
+ image: ({ value }) => <SanityImage image={value} maxWidth={1920} />,
45
+ table: ({ value }) => {
46
+ const rows = value?.rows as
47
+ | Array<{ _key?: string; cells?: string[] }>
48
+ | undefined;
49
+ if (!rows?.length) return null;
50
+ return (
51
+ <table>
52
+ <tbody>
53
+ {rows.map((row, i) => (
54
+ <tr key={row._key ?? `row-${i}`}>
55
+ {row.cells?.map((cell) => {
56
+ const cellKey = `${row._key}-${cell}`;
57
+ return i === 0 ? (
58
+ <th key={cellKey}>{cell}</th>
59
+ ) : (
60
+ <td key={cellKey}>{cell}</td>
61
+ );
62
+ })}
63
+ </tr>
64
+ ))}
65
+ </tbody>
66
+ </table>
67
+ );
68
+ },
69
+ },
70
+ marks: {
71
+ link: LinkMark,
72
+ pageReference: LinkMark,
73
+ externalLink: LinkMark,
74
+ },
75
+ block: {
76
+ h1: ({ children }) => <h1 className="h1">{children}</h1>,
77
+ h2: ({ children }) => <h2 className="h2">{children}</h2>,
78
+ h3: ({ children }) => <h3 className="h3">{children}</h3>,
79
+ h4: ({ children }) => <h4 className="h4">{children}</h4>,
80
+ h5: ({ children }) => <h5 className="h5">{children}</h5>,
81
+ h6: ({ children }) => <h6 className="h6">{children}</h6>,
82
+ normal: ({ children }) => <p className="p">{children}</p>,
83
+ },
84
+ }}
85
+ />
86
+ );
87
+ };
@@ -0,0 +1,28 @@
1
+ import { draftMode } from "next/headers";
2
+ import { VisualEditing } from "next-sanity/visual-editing";
3
+ import { Suspense } from "react";
4
+ import { refreshAction } from "@/app/actions/refresh";
5
+ import { DraftModeToggle } from "@/components/sanity/draft-mode-toggle";
6
+ import { isSanityConfigured } from "@/lib/integrations/check-integration";
7
+ import { SanityLive } from "@/lib/integrations/sanity/live";
8
+
9
+ async function VisualEditingInner() {
10
+ const { isEnabled: isDraftMode } = await draftMode();
11
+ const sanityConfigured = isSanityConfigured();
12
+
13
+ if (!(sanityConfigured && isDraftMode)) return null;
14
+
15
+ return (
16
+ <>
17
+ <DraftModeToggle />
18
+ <VisualEditing />
19
+ <SanityLive action={refreshAction} />
20
+ </>
21
+ );
22
+ }
23
+
24
+ export const SanityVisualEditing = () => (
25
+ <Suspense fallback={null}>
26
+ <VisualEditingInner />
27
+ </Suspense>
28
+ );
@@ -0,0 +1,41 @@
1
+ import { getImageDimensions } from "@sanity/asset-utils";
2
+ import { Image, type ImageProps } from "@/components/ui/image";
3
+ import { urlForImage } from "@/lib/integrations/sanity/utils/image";
4
+
5
+ interface SanityImageProps extends Omit<ImageProps, "src" | "aspectRatio"> {
6
+ image: {
7
+ asset?: {
8
+ _ref: string;
9
+ _type: "reference";
10
+ };
11
+ alt?: string;
12
+ hotspot?: object;
13
+ crop?: object;
14
+ };
15
+ maxWidth?: number;
16
+ }
17
+
18
+ export function SanityImage({
19
+ image,
20
+ maxWidth = 1920,
21
+ alt,
22
+ fill,
23
+ ...props
24
+ }: SanityImageProps) {
25
+ if (!image?.asset) return null;
26
+
27
+ const { width, height } = getImageDimensions(image.asset);
28
+ const aspectRatio = width / height;
29
+
30
+ const imageProps = fill ? { fill: true } : { width, height };
31
+
32
+ return (
33
+ <Image
34
+ src={urlForImage(image).width(maxWidth).url()}
35
+ alt={alt || image.alt || ""}
36
+ aspectRatio={aspectRatio}
37
+ {...imageProps}
38
+ {...props}
39
+ />
40
+ );
41
+ }
@@ -0,0 +1,5 @@
1
+ export const isSanityConfigured = () =>
2
+ Boolean(
3
+ process.env.NEXT_PUBLIC_SANITY_PROJECT_ID &&
4
+ process.env.NEXT_PUBLIC_SANITY_DATASET,
5
+ );
@@ -0,0 +1,27 @@
1
+ import { createClient, type SanityClient } from "next-sanity";
2
+ import { isSanityConfigured } from "@/lib/integrations/check-integration";
3
+ import { apiVersion, dataset, projectId, sanityToken, studioUrl } from "./env";
4
+
5
+ /**
6
+ * Sanity client instance
7
+ *
8
+ * Returns null if Sanity is not configured (missing env vars).
9
+ * Always check with isSanityConfigured() before using.
10
+ */
11
+ export const client: SanityClient | null = isSanityConfigured()
12
+ ? createClient({
13
+ projectId,
14
+ dataset,
15
+ apiVersion,
16
+ useCdn: true,
17
+ perspective: "published",
18
+ token: sanityToken,
19
+ stega: {
20
+ studioUrl,
21
+ filter: (props) => {
22
+ if (props.sourcePath.at(-1) === "title") return true;
23
+ return props.filterDefault(props);
24
+ },
25
+ },
26
+ })
27
+ : null;
@@ -0,0 +1,23 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { usePathname } from "next/navigation";
5
+ import { useIsPresentationTool } from "next-sanity/hooks";
6
+
7
+ export function DisableDraftMode() {
8
+ const isPresentationTool = useIsPresentationTool();
9
+ const pathname = usePathname();
10
+
11
+ if (isPresentationTool) return null;
12
+ if (pathname.startsWith("/studio")) return null;
13
+
14
+ return (
15
+ <Link
16
+ href="/api/draft-mode/disable"
17
+ scroll={false}
18
+ className="dr-p-4 fixed top-safe right-safe z-50 bg-red font-mono text-primary text-sm uppercase"
19
+ >
20
+ Disable Draft Mode
21
+ </Link>
22
+ );
23
+ }
@@ -0,0 +1,39 @@
1
+ import { type ArrayOfObjectsInputProps, useFormValue } from "sanity";
2
+ import {
3
+ getVisiblePageBuilderInsertMenuGroups,
4
+ getVisiblePageBuilderReferenceMembers,
5
+ type PageBuilderGroupName,
6
+ } from "@/lib/integrations/sanity/page-builder-config";
7
+
8
+ export function PageBuilderInput(props: ArrayOfObjectsInputProps) {
9
+ const pageTypeValue = useFormValue(["type"]);
10
+ const pageType =
11
+ typeof pageTypeValue === "string"
12
+ ? (pageTypeValue as PageBuilderGroupName)
13
+ : undefined;
14
+
15
+ const visibleMemberNames = new Set(
16
+ getVisiblePageBuilderReferenceMembers(pageType).map(
17
+ (member) => member.name,
18
+ ),
19
+ );
20
+
21
+ return props.renderDefault({
22
+ ...props,
23
+ schemaType: {
24
+ ...props.schemaType,
25
+ of: props.schemaType.of.filter(
26
+ (member) =>
27
+ typeof member.name === "string" &&
28
+ visibleMemberNames.has(member.name),
29
+ ),
30
+ options: {
31
+ ...props.schemaType.options,
32
+ insertMenu: {
33
+ ...props.schemaType.options?.insertMenu,
34
+ groups: getVisiblePageBuilderInsertMenuGroups(pageType),
35
+ },
36
+ },
37
+ },
38
+ });
39
+ }
@@ -0,0 +1,50 @@
1
+ import { Select } from "@sanity/ui";
2
+ import { useEffect } from "react";
3
+ import { type StringInputProps, set, setIfMissing } from "sanity";
4
+
5
+ type PageCategoryOption = {
6
+ title: string;
7
+ value: string;
8
+ };
9
+
10
+ const isPageCategoryOption = (option: unknown): option is PageCategoryOption =>
11
+ typeof option === "object" &&
12
+ option !== null &&
13
+ "title" in option &&
14
+ "value" in option &&
15
+ typeof option.title === "string" &&
16
+ typeof option.value === "string";
17
+
18
+ export function PageCategoryInput(props: StringInputProps) {
19
+ useEffect(() => {
20
+ if (props.value === undefined) {
21
+ props.onChange(setIfMissing("generic"));
22
+ }
23
+ }, [props.onChange, props.value]);
24
+
25
+ const options = Array.isArray(props.schemaType.options?.list)
26
+ ? props.schemaType.options.list.filter(isPageCategoryOption)
27
+ : [];
28
+
29
+ if (options.length === 0) {
30
+ return props.renderDefault({ ...props, value: props.value ?? "generic" });
31
+ }
32
+
33
+ return (
34
+ <Select
35
+ {...props.elementProps}
36
+ disabled={props.readOnly}
37
+ onChange={(event) => props.onChange(set(event.currentTarget.value))}
38
+ value={props.value ?? "generic"}
39
+ {...(props.validationError
40
+ ? { customValidity: props.validationError }
41
+ : {})}
42
+ >
43
+ {options.map((option) => (
44
+ <option key={option.value} value={option.value}>
45
+ {option.title}
46
+ </option>
47
+ ))}
48
+ </Select>
49
+ );
50
+ }
@@ -0,0 +1,18 @@
1
+ import { type FieldProps, useFormValue } from "sanity";
2
+ import { getPinnedPageById } from "@/lib/integrations/sanity/pinned-pages";
3
+
4
+ const DEFAULT_DESCRIPTION = "URL path without leading slash.";
5
+ const PINNED_DESCRIPTION =
6
+ "URL path without leading slash. Disabled on 📌 pinned pages.";
7
+
8
+ // Swaps the slug description to a locked-state note on pinned pages.
9
+ // (Sanity descriptions are plain strings, hence the 📌 character.)
10
+ export function SlugField(props: FieldProps) {
11
+ const documentId = useFormValue(["_id"]) as string | undefined;
12
+ const isPinned = getPinnedPageById(documentId) !== null;
13
+
14
+ return props.renderDefault({
15
+ ...props,
16
+ description: isPinned ? PINNED_DESCRIPTION : DEFAULT_DESCRIPTION,
17
+ });
18
+ }
@@ -0,0 +1,14 @@
1
+ import type { SlugInputProps } from "sanity";
2
+
3
+ // Adds a not-allowed cursor to the slug field when it's read-only (pinned pages).
4
+ export function SlugInput(props: SlugInputProps) {
5
+ if (!props.readOnly) {
6
+ return props.renderDefault(props);
7
+ }
8
+
9
+ return (
10
+ <div style={{ cursor: "not-allowed" }}>
11
+ <div style={{ pointerEvents: "none" }}>{props.renderDefault(props)}</div>
12
+ </div>
13
+ );
14
+ }
@@ -0,0 +1,40 @@
1
+ import { useState } from "react";
2
+ import type {
3
+ DocumentActionComponent,
4
+ DocumentActionDialogProps,
5
+ DocumentActionProps,
6
+ } from "sanity";
7
+
8
+ export function createConfirmPublishAction(
9
+ originalPublishAction: DocumentActionComponent,
10
+ ): DocumentActionComponent {
11
+ const ConfirmPublishAction = (props: DocumentActionProps) => {
12
+ const original = originalPublishAction(props);
13
+ const [dialogOpen, setDialogOpen] = useState(false);
14
+
15
+ if (!original) return null;
16
+
17
+ const { dialog: _originalDialog, ...originalWithoutDialog } = original;
18
+ const dialog: DocumentActionDialogProps | false = dialogOpen
19
+ ? {
20
+ type: "confirm",
21
+ tone: "critical",
22
+ message:
23
+ "This will make these changes live on the production site immediately. Continue?",
24
+ onCancel: () => setDialogOpen(false),
25
+ onConfirm: () => {
26
+ setDialogOpen(false);
27
+ original.onHandle?.();
28
+ },
29
+ }
30
+ : false;
31
+
32
+ return {
33
+ ...originalWithoutDialog,
34
+ onHandle: () => setDialogOpen(true),
35
+ dialog,
36
+ };
37
+ };
38
+
39
+ return ConfirmPublishAction;
40
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Sanity Environment Configuration
3
+ *
4
+ * All values have safe defaults to allow the app to build and run
5
+ * without Sanity configured. Use isSanityConfigured() to check
6
+ * if Sanity is properly set up before making API calls.
7
+ */
8
+
9
+ /** Sanity API version - defaults to latest stable */
10
+ export const apiVersion =
11
+ process.env.NEXT_PUBLIC_SANITY_API_VERSION || "2024-03-15";
12
+
13
+ /** Sanity dataset name - defaults to 'production' */
14
+ export const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET || "production";
15
+
16
+ /** Sanity project ID - empty string if not configured */
17
+ export const projectId =
18
+ process.env.NEXT_PUBLIC_SANITY_PROJECT_ID ||
19
+ process.env.SANITY_STUDIO_PROJECT_ID ||
20
+ "";
21
+
22
+ /** Sanity Studio URL for visual editing */
23
+ export const studioUrl =
24
+ process.env.NODE_ENV === "development"
25
+ ? "http://localhost:3000/studio"
26
+ : `${process.env.NEXT_PUBLIC_BASE_URL || ""}/studio`;
27
+
28
+ export const sanityToken = process.env.SANITY_API_READ_TOKEN || "";
29
+
30
+ /** Preview URL for draft mode */
31
+ export const previewURL =
32
+ process.env.NODE_ENV === "development"
33
+ ? "http://localhost:3000"
34
+ : process.env.NEXT_PUBLIC_BASE_URL || "";
@@ -0,0 +1,48 @@
1
+ import { cache } from "react";
2
+ import { fetchSanity } from "@/lib/integrations/sanity/live";
3
+ import {
4
+ COMPANY_DATA_QUERY,
5
+ FOOTER_QUERY,
6
+ NAVBAR_QUERY,
7
+ } from "@/lib/integrations/sanity/queries";
8
+ import type {
9
+ COMPANY_DATA_QUERY_RESULT,
10
+ FOOTER_QUERY_RESULT,
11
+ NAVBAR_QUERY_RESULT,
12
+ } from "@/lib/integrations/sanity/sanity.types";
13
+
14
+ export type NavbarData = NonNullable<NAVBAR_QUERY_RESULT>;
15
+ export type FooterData = NonNullable<FOOTER_QUERY_RESULT>;
16
+ export type CompanyData = NonNullable<COMPANY_DATA_QUERY_RESULT>;
17
+
18
+ // Singleton document types that, when published, should bust the layout cache.
19
+ export const SANITY_LAYOUT_TAGS = ["navbar", "footer", "companyData"];
20
+
21
+ // Wrapped in React cache so components sharing a singleton (e.g. Footer + JsonLd
22
+ // both reading companyData) only hit Sanity once per request.
23
+ export const getNavbarData = cache(async (): Promise<NavbarData | null> => {
24
+ const { data } = await fetchSanity<NavbarData>({
25
+ query: NAVBAR_QUERY,
26
+ tags: ["navbar"],
27
+ });
28
+
29
+ return data ?? null;
30
+ });
31
+
32
+ export const getFooterData = cache(async (): Promise<FooterData | null> => {
33
+ const { data } = await fetchSanity<FooterData>({
34
+ query: FOOTER_QUERY,
35
+ tags: ["footer"],
36
+ });
37
+
38
+ return data ?? null;
39
+ });
40
+
41
+ export const getCompanyData = cache(async (): Promise<CompanyData | null> => {
42
+ const { data } = await fetchSanity<CompanyData>({
43
+ query: COMPANY_DATA_QUERY,
44
+ tags: ["companyData"],
45
+ });
46
+
47
+ return data ?? null;
48
+ });
@@ -0,0 +1,17 @@
1
+ import { fetchSanity } from "@/lib/integrations/sanity/live";
2
+ import { REDIRECTS_QUERY } from "@/lib/integrations/sanity/queries";
3
+ import type { REDIRECTS_QUERY_RESULT } from "@/lib/integrations/sanity/sanity.types";
4
+
5
+ export type RedirectEntry = REDIRECTS_QUERY_RESULT[number];
6
+
7
+ /** Cache tag invalidated by the revalidate webhook on publish. */
8
+ export const SANITY_REDIRECTS_TAG = "redirect";
9
+
10
+ export const getRedirects = async (): Promise<REDIRECTS_QUERY_RESULT> => {
11
+ const { data } = await fetchSanity<REDIRECTS_QUERY_RESULT>({
12
+ query: REDIRECTS_QUERY,
13
+ tags: [SANITY_REDIRECTS_TAG],
14
+ });
15
+
16
+ return data ?? [];
17
+ };