bsmnt 0.2.11 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/dist/helpers/create/copy-template.d.ts +1 -1
  2. package/dist/helpers/create/copy-template.d.ts.map +1 -1
  3. package/dist/helpers/create/index.d.ts.map +1 -1
  4. package/dist/helpers/create/index.js +2 -1
  5. package/dist/helpers/create/index.js.map +1 -1
  6. package/dist/helpers/integrate/merge-config.d.ts.map +1 -1
  7. package/dist/helpers/integrate/merge-config.js +0 -2
  8. package/dist/helpers/integrate/merge-config.js.map +1 -1
  9. package/dist/helpers/integrate/sanity/config.d.ts.map +1 -1
  10. package/dist/helpers/integrate/sanity/config.js +2 -4
  11. package/dist/helpers/integrate/sanity/config.js.map +1 -1
  12. package/dist/index.js +84 -35
  13. package/dist/index.js.map +1 -1
  14. package/index.js +2 -2
  15. package/package.json +1 -1
  16. package/src/templates/next-default/.vscode/settings.json +1 -1
  17. package/src/templates/next-default/README.md +6 -7
  18. package/src/templates/next-default/app/layout.tsx +17 -4
  19. package/src/templates/next-default/biome.json +1 -1
  20. package/src/templates/next-default/css.d.ts +1 -0
  21. package/src/templates/next-default/lib/README.md +4 -8
  22. package/src/templates/next-default/lib/hooks/use-media.ts +3 -1
  23. package/src/templates/next-default/lib/styles/global.css +182 -0
  24. package/src/templates/next-default/lib/utils/json-ld.tsx +13 -18
  25. package/src/templates/next-default/lib/utils/portable-text-to-markdown.ts +83 -0
  26. package/src/templates/next-default/package.json +3 -3
  27. package/src/templates/next-experiments/.vscode/settings.json +1 -1
  28. package/src/templates/next-experiments/README.md +6 -7
  29. package/src/templates/next-experiments/app/layout.tsx +17 -4
  30. package/src/templates/next-experiments/biome.json +1 -1
  31. package/src/templates/next-experiments/css.d.ts +1 -0
  32. package/src/templates/next-experiments/lib/README.md +4 -8
  33. package/src/templates/next-experiments/lib/hooks/use-media.ts +3 -1
  34. package/src/templates/next-experiments/lib/styles/global.css +182 -0
  35. package/src/templates/next-experiments/lib/utils/json-ld.tsx +13 -18
  36. package/src/templates/next-experiments/lib/utils/portable-text-to-markdown.ts +83 -0
  37. package/src/templates/next-experiments/package.json +3 -3
  38. package/src/templates/next-pagebuilder/.env.example +11 -0
  39. package/src/templates/next-pagebuilder/README.md +23 -0
  40. package/src/templates/next-pagebuilder/_gitignore +67 -0
  41. package/src/templates/next-pagebuilder/app/(content)/[[...slug]]/page.tsx +68 -0
  42. package/src/templates/next-pagebuilder/app/(content)/layout.tsx +13 -0
  43. package/src/templates/next-pagebuilder/app/api/[[...slug]]/route.ts +100 -0
  44. package/src/templates/next-pagebuilder/app/api/draft-mode/disable/route.ts +7 -0
  45. package/src/templates/next-pagebuilder/app/api/draft-mode/enable/route.ts +20 -0
  46. package/src/templates/next-pagebuilder/app/api/revalidate/route.ts +121 -0
  47. package/src/templates/next-pagebuilder/app/favicon.ico +0 -0
  48. package/src/templates/next-pagebuilder/app/layout.tsx +80 -0
  49. package/src/templates/next-pagebuilder/app/robots.ts +15 -0
  50. package/src/templates/next-pagebuilder/app/sitemap.md/route.ts +124 -0
  51. package/src/templates/next-pagebuilder/app/sitemap.xml/route.ts +80 -0
  52. package/src/templates/next-pagebuilder/app/studio/[[...tool]]/page.tsx +8 -0
  53. package/src/templates/next-pagebuilder/biome.json +239 -0
  54. package/src/templates/next-pagebuilder/components/layout/footer/index.tsx +95 -0
  55. package/src/templates/next-pagebuilder/components/layout/header/components/cta-button.tsx +28 -0
  56. package/src/templates/next-pagebuilder/components/layout/header/components/mega-menu-panel.tsx +90 -0
  57. package/src/templates/next-pagebuilder/components/layout/header/components/nav-item-renderer.tsx +98 -0
  58. package/src/templates/next-pagebuilder/components/layout/header/components/nav-leaf-item.tsx +33 -0
  59. package/src/templates/next-pagebuilder/components/layout/header/components/types.ts +7 -0
  60. package/src/templates/next-pagebuilder/components/layout/header/header-client.tsx +110 -0
  61. package/src/templates/next-pagebuilder/components/layout/header/index.tsx +8 -0
  62. package/src/templates/next-pagebuilder/components/layout/json-ld/index.tsx +45 -0
  63. package/src/templates/next-pagebuilder/components/layout/wrapper/index.tsx +30 -0
  64. package/src/templates/next-pagebuilder/components/page-builder/components/article-content/index.tsx +83 -0
  65. package/src/templates/next-pagebuilder/components/page-builder/components/article-content/related-post-item.tsx +27 -0
  66. package/src/templates/next-pagebuilder/components/page-builder/components/description.tsx +17 -0
  67. package/src/templates/next-pagebuilder/components/page-builder/components/hero.tsx +17 -0
  68. package/src/templates/next-pagebuilder/components/page-builder/components/post-collection/content-card.tsx +66 -0
  69. package/src/templates/next-pagebuilder/components/page-builder/components/post-collection/content-grid.tsx +42 -0
  70. package/src/templates/next-pagebuilder/components/page-builder/components/post-collection/index.tsx +28 -0
  71. package/src/templates/next-pagebuilder/components/page-builder/components/post-collection/types.ts +16 -0
  72. package/src/templates/next-pagebuilder/components/page-builder/renderer.tsx +36 -0
  73. package/src/templates/next-pagebuilder/components/page-builder/types.ts +23 -0
  74. package/src/templates/next-pagebuilder/components/page-document/index.tsx +91 -0
  75. package/src/templates/next-pagebuilder/components/sanity/draft-mode-toggle.tsx +27 -0
  76. package/src/templates/next-pagebuilder/components/sanity/rich-text.tsx +87 -0
  77. package/src/templates/next-pagebuilder/components/sanity/visual-editing.tsx +27 -0
  78. package/src/templates/next-pagebuilder/components/ui/image/index.tsx +216 -0
  79. package/src/templates/next-pagebuilder/components/ui/link/index.tsx +152 -0
  80. package/src/templates/next-pagebuilder/components/ui/sanity-image/index.tsx +41 -0
  81. package/src/templates/next-pagebuilder/lib/integrations/check-integration.ts +5 -0
  82. package/src/templates/next-pagebuilder/lib/integrations/sanity/client.ts +27 -0
  83. package/src/templates/next-pagebuilder/lib/integrations/sanity/components/disable-draft-mode.tsx +23 -0
  84. package/src/templates/next-pagebuilder/lib/integrations/sanity/components/page-builder-input.tsx +36 -0
  85. package/src/templates/next-pagebuilder/lib/integrations/sanity/components/page-category-input.tsx +50 -0
  86. package/src/templates/next-pagebuilder/lib/integrations/sanity/components/rich-text.tsx +84 -0
  87. package/src/templates/next-pagebuilder/lib/integrations/sanity/confirm-publish-action.ts +40 -0
  88. package/src/templates/next-pagebuilder/lib/integrations/sanity/env.ts +34 -0
  89. package/src/templates/next-pagebuilder/lib/integrations/sanity/fetchers/layout.ts +35 -0
  90. package/src/templates/next-pagebuilder/lib/integrations/sanity/icons.ts +58 -0
  91. package/src/templates/next-pagebuilder/lib/integrations/sanity/live/index.tsx +61 -0
  92. package/src/templates/next-pagebuilder/lib/integrations/sanity/markdown-proxy.config.ts +50 -0
  93. package/src/templates/next-pagebuilder/lib/integrations/sanity/page-builder-config.ts +132 -0
  94. package/src/templates/next-pagebuilder/lib/integrations/sanity/page-category.ts +28 -0
  95. package/src/templates/next-pagebuilder/lib/integrations/sanity/queries.ts +281 -0
  96. package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.cli.ts +29 -0
  97. package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.config.ts +211 -0
  98. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/components/index.ts +4 -0
  99. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/components/reusable/blog-content.ts +89 -0
  100. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/components/reusable/description.ts +29 -0
  101. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/components/reusable/hero.ts +28 -0
  102. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/components/singleton/content-collection.ts +45 -0
  103. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/content/author.ts +70 -0
  104. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/content/blog-category.ts +55 -0
  105. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/index.ts +96 -0
  106. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/layout/company-data.ts +62 -0
  107. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/layout/footer.ts +79 -0
  108. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/layout/navbar.ts +74 -0
  109. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/link.ts +125 -0
  110. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/logo-field.ts +9 -0
  111. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/metadata.ts +68 -0
  112. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/nav-objects.ts +192 -0
  113. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/page-builder.ts +39 -0
  114. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/page-folder.ts +124 -0
  115. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/page.ts +232 -0
  116. package/src/templates/next-pagebuilder/lib/integrations/sanity/schemas/shared/richText.ts +63 -0
  117. package/src/templates/next-pagebuilder/lib/integrations/sanity/singletons.ts +44 -0
  118. package/src/templates/next-pagebuilder/lib/integrations/sanity/structure.ts +453 -0
  119. package/src/templates/next-pagebuilder/lib/integrations/sanity/utils/image.ts +8 -0
  120. package/src/templates/next-pagebuilder/lib/integrations/sanity/utils/link.ts +137 -0
  121. package/src/templates/next-pagebuilder/lib/integrations/sanity/utils/page-builder-markdown.ts +81 -0
  122. package/src/templates/next-pagebuilder/lib/scripts/sanity-typegen.ts +45 -0
  123. package/src/templates/next-pagebuilder/lib/styles/cn.ts +5 -0
  124. package/src/templates/next-pagebuilder/lib/styles/global.css +70 -0
  125. package/src/templates/next-pagebuilder/lib/utils/base-url.ts +17 -0
  126. package/src/templates/next-pagebuilder/lib/utils/format-date.ts +8 -0
  127. package/src/templates/next-pagebuilder/lib/utils/json-ld.tsx +213 -0
  128. package/src/templates/next-pagebuilder/lib/utils/metadata.ts +167 -0
  129. package/src/templates/next-pagebuilder/lib/utils/sitemap.ts +37 -0
  130. package/src/templates/next-pagebuilder/lib/utils/slug-tag.ts +6 -0
  131. package/src/templates/next-pagebuilder/next.config.ts +134 -0
  132. package/src/templates/next-pagebuilder/package.json +71 -0
  133. package/src/templates/next-pagebuilder/postcss.config.mjs +39 -0
  134. package/src/templates/next-pagebuilder/proxy.ts +81 -0
  135. package/src/templates/next-pagebuilder/svg.d.ts +5 -0
  136. package/src/templates/next-pagebuilder/tsconfig.json +38 -0
  137. package/src/templates/next-webgl/.vscode/settings.json +1 -1
  138. package/src/templates/next-webgl/README.md +6 -7
  139. package/src/templates/next-webgl/app/layout.tsx +17 -4
  140. package/src/templates/next-webgl/biome.json +1 -1
  141. package/src/templates/next-webgl/css.d.ts +1 -0
  142. package/src/templates/next-webgl/lib/README.md +4 -8
  143. package/src/templates/next-webgl/lib/hooks/use-media.ts +3 -1
  144. package/src/templates/next-webgl/lib/styles/global.css +182 -0
  145. package/src/templates/next-webgl/lib/utils/json-ld.tsx +13 -18
  146. package/src/templates/next-webgl/lib/utils/portable-text-to-markdown.ts +83 -0
  147. package/src/templates/next-webgl/package.json +3 -3
  148. package/src/helpers/integrate/sanity/files/lib/scripts/copy-sanity-mcp.ts +0 -23
  149. package/src/helpers/integrate/sanity/files/lib/scripts/generate-page.ts +0 -297
  150. package/src/templates/next-default/lib/scripts/dev.ts +0 -32
  151. package/src/templates/next-default/lib/styles/README.md +0 -13
  152. package/src/templates/next-default/lib/styles/fonts.ts +0 -20
  153. package/src/templates/next-default/lib/styles/index.css +0 -3
  154. package/src/templates/next-default/lib/styles/tokens.css +0 -179
  155. package/src/templates/next-default/lib/utils/README.md +0 -40
  156. package/src/templates/next-default/lib/utils/easings.ts +0 -240
  157. package/src/templates/next-default/lib/utils/fetch.ts +0 -84
  158. package/src/templates/next-default/lib/utils/global-css.d.ts +0 -1
  159. package/src/templates/next-default/lib/utils/math.ts +0 -236
  160. package/src/templates/next-default/lib/utils/strings.ts +0 -246
  161. package/src/templates/next-default/lib/utils/types.d.ts +0 -15
  162. package/src/templates/next-default/lib/utils/viewport.ts +0 -199
  163. package/src/templates/next-experiments/lib/scripts/dev.ts +0 -32
  164. package/src/templates/next-experiments/lib/styles/README.md +0 -13
  165. package/src/templates/next-experiments/lib/styles/fonts.ts +0 -20
  166. package/src/templates/next-experiments/lib/styles/index.css +0 -3
  167. package/src/templates/next-experiments/lib/styles/tokens.css +0 -179
  168. package/src/templates/next-experiments/lib/utils/README.md +0 -40
  169. package/src/templates/next-experiments/lib/utils/easings.ts +0 -240
  170. package/src/templates/next-experiments/lib/utils/fetch.ts +0 -84
  171. package/src/templates/next-experiments/lib/utils/global-css.d.ts +0 -1
  172. package/src/templates/next-experiments/lib/utils/math.ts +0 -236
  173. package/src/templates/next-experiments/lib/utils/strings.ts +0 -246
  174. package/src/templates/next-experiments/lib/utils/types.d.ts +0 -15
  175. package/src/templates/next-experiments/lib/utils/viewport.ts +0 -199
  176. package/src/templates/next-webgl/lib/scripts/dev.ts +0 -32
  177. package/src/templates/next-webgl/lib/styles/README.md +0 -13
  178. package/src/templates/next-webgl/lib/styles/fonts.ts +0 -20
  179. package/src/templates/next-webgl/lib/styles/index.css +0 -3
  180. package/src/templates/next-webgl/lib/styles/tokens.css +0 -179
  181. package/src/templates/next-webgl/lib/utils/README.md +0 -40
  182. package/src/templates/next-webgl/lib/utils/easings.ts +0 -240
  183. package/src/templates/next-webgl/lib/utils/fetch.ts +0 -84
  184. package/src/templates/next-webgl/lib/utils/global-css.d.ts +0 -1
  185. package/src/templates/next-webgl/lib/utils/math.ts +0 -236
  186. package/src/templates/next-webgl/lib/utils/strings.ts +0 -246
  187. package/src/templates/next-webgl/lib/utils/types.d.ts +0 -15
  188. package/src/templates/next-webgl/lib/utils/viewport.ts +0 -199
@@ -0,0 +1,453 @@
1
+ import type {
2
+ ListBuilder,
3
+ ListItemBuilder,
4
+ StructureBuilder,
5
+ StructureResolver,
6
+ StructureResolverContext,
7
+ } from "sanity/structure"
8
+ import { apiVersion } from "./env"
9
+ import {
10
+ authorIcon,
11
+ blogCategoryIcon,
12
+ componentsFolderIcon,
13
+ contentFolderIcon,
14
+ contentIcon,
15
+ descriptionIcon,
16
+ heroIcon,
17
+ layoutFolderIcon,
18
+ pageFolderIcon,
19
+ pageIcon,
20
+ pagesFolderIcon,
21
+ } from "./icons"
22
+ import {
23
+ getPageCategoryIcon,
24
+ type PageBuilderGroupName,
25
+ } from "./page-builder-config"
26
+ import { pageCategoryExpression } from "./page-category"
27
+ import { singletonComponents, singletonLayout } from "./singletons"
28
+
29
+ type PageTreeItem = {
30
+ _id: string
31
+ _type: "page" | "pageFolder"
32
+ title?: string | null
33
+ slug?: string | null
34
+ pageType?: PageBuilderGroupName | null
35
+ }
36
+
37
+ const PAGE_TREE_ITEM_ID_PREFIX = {
38
+ page: "page-tree-",
39
+ pageFolder: "page-folder-tree-",
40
+ } as const
41
+
42
+ const PAGE_TREE_QUERY = `
43
+ *[
44
+ (
45
+ _type == "pageFolder" &&
46
+ (
47
+ ($folderId == null && !defined(parentFolder._ref)) ||
48
+ parentFolder._ref == $folderId
49
+ )
50
+ ) ||
51
+ (
52
+ _type == "page" &&
53
+ (
54
+ ($folderId == null && !defined(pageFolder._ref)) ||
55
+ pageFolder._ref == $folderId
56
+ )
57
+ )
58
+ ]{
59
+ _id,
60
+ _type,
61
+ title,
62
+ "slug": slug.current,
63
+ "pageType": ${pageCategoryExpression}
64
+ }
65
+ `
66
+
67
+ const normalizeDocumentId = (documentId: string) =>
68
+ documentId.replace(/^drafts\./, "")
69
+
70
+ const getPageTreeItemId = (item: Pick<PageTreeItem, "_id" | "_type">) => {
71
+ const normalizedId = normalizeDocumentId(item._id)
72
+
73
+ return `${PAGE_TREE_ITEM_ID_PREFIX[item._type]}${normalizedId}`
74
+ }
75
+
76
+ export const getDocumentIdFromPageTreeItemId = (itemId: string) =>
77
+ normalizeDocumentId(
78
+ itemId
79
+ .replace(PAGE_TREE_ITEM_ID_PREFIX.page, "")
80
+ .replace(PAGE_TREE_ITEM_ID_PREFIX.pageFolder, "")
81
+ )
82
+
83
+ const getPageTreeTemplateIds = (folderId: string | null) =>
84
+ folderId === null
85
+ ? ["page", "pageFolder"]
86
+ : ["page-with-folder", "page-folder-with-parent"]
87
+
88
+ const buildPageTreeCreateChild = (
89
+ S: StructureBuilder,
90
+ documentId: string,
91
+ folderId: string | null,
92
+ templateId: string
93
+ ) => {
94
+ if (templateId === "page") {
95
+ return S.documentWithInitialValueTemplate("page").documentId(documentId)
96
+ }
97
+
98
+ if (templateId === "pageFolder") {
99
+ return S.documentWithInitialValueTemplate("pageFolder").documentId(
100
+ documentId
101
+ )
102
+ }
103
+
104
+ if (templateId === "page-with-folder") {
105
+ return S.documentWithInitialValueTemplate("page-with-folder", {
106
+ pageFolderId: folderId,
107
+ }).documentId(documentId)
108
+ }
109
+
110
+ if (templateId === "page-folder-with-parent") {
111
+ return S.documentWithInitialValueTemplate("page-folder-with-parent", {
112
+ parentFolderId: folderId,
113
+ }).documentId(documentId)
114
+ }
115
+
116
+ return undefined
117
+ }
118
+
119
+ const getPageTreeClient = (context: StructureResolverContext) =>
120
+ context
121
+ .getClient({ apiVersion })
122
+ .withConfig({ perspective: context.perspectiveStack })
123
+
124
+ const getPageItemTitle = (page: PageTreeItem) => {
125
+ return `${page.title || "Untitled Page"} (${`/${page.slug || ""}`})`
126
+ }
127
+
128
+ const getFolderItemTitle = (folder: PageTreeItem) =>
129
+ folder.slug
130
+ ? `${folder.title || "Untitled Folder"} (${`/${folder.slug}`})`
131
+ : folder.title || "Untitled Folder"
132
+
133
+ const sortPageTreeItems = (items: PageTreeItem[]) =>
134
+ [...items].sort((left, right) => {
135
+ if (left._type !== right._type) {
136
+ return left._type === "page" ? -1 : 1
137
+ }
138
+
139
+ const leftLabel =
140
+ left._type === "page" ? getPageItemTitle(left) : getFolderItemTitle(left)
141
+ const rightLabel =
142
+ right._type === "page"
143
+ ? getPageItemTitle(right)
144
+ : getFolderItemTitle(right)
145
+
146
+ if (leftLabel === "/") return -1
147
+ if (rightLabel === "/") return 1
148
+
149
+ return leftLabel.localeCompare(rightLabel)
150
+ })
151
+
152
+ async function fetchPageTreeItems(
153
+ context: StructureResolverContext,
154
+ folderId: string | null
155
+ ) {
156
+ return getPageTreeClient(context).fetch<PageTreeItem[]>(PAGE_TREE_QUERY, {
157
+ folderId,
158
+ })
159
+ }
160
+
161
+ function buildPageTreeList(
162
+ S: StructureBuilder,
163
+ context: StructureResolverContext,
164
+ folderId: string | null,
165
+ title: string
166
+ ): Promise<ListBuilder> {
167
+ return fetchPageTreeItems(context, folderId).then((items) => {
168
+ const currentFolderId = folderId
169
+ const sortedItems = sortPageTreeItems(items)
170
+ const allowedTemplateIds = getPageTreeTemplateIds(currentFolderId)
171
+ const childItems: ListItemBuilder[] = sortedItems.map((item) => {
172
+ const normalizedId = normalizeDocumentId(item._id)
173
+
174
+ if (item._type === "pageFolder") {
175
+ return S.listItem()
176
+ .id(getPageTreeItemId(item))
177
+ .title(getFolderItemTitle(item))
178
+ .icon(pageFolderIcon)
179
+ .child(() =>
180
+ buildPageTreeList(
181
+ S,
182
+ context,
183
+ item._id,
184
+ item.title || "Untitled Folder"
185
+ )
186
+ )
187
+ }
188
+
189
+ return S.listItem()
190
+ .id(getPageTreeItemId(item))
191
+ .title(getPageItemTitle(item))
192
+ .icon(getPageCategoryIcon(item.pageType))
193
+ .child(S.document().schemaType("page").documentId(normalizedId))
194
+ })
195
+
196
+ const initialValueTemplates =
197
+ currentFolderId === null
198
+ ? [
199
+ S.initialValueTemplateItem("page"),
200
+ S.initialValueTemplateItem("pageFolder"),
201
+ ]
202
+ : [
203
+ S.initialValueTemplateItem("page-with-folder", {
204
+ pageFolderId: currentFolderId,
205
+ }),
206
+ S.initialValueTemplateItem("page-folder-with-parent", {
207
+ parentFolderId: currentFolderId,
208
+ }),
209
+ ]
210
+
211
+ const createMenuItems =
212
+ currentFolderId === null
213
+ ? [
214
+ S.menuItem()
215
+ .title("Create Page")
216
+ .icon(pageIcon)
217
+ .intent({
218
+ type: "create",
219
+ params: {
220
+ type: "page",
221
+ template: "page",
222
+ },
223
+ }),
224
+ S.menuItem()
225
+ .title("Create Folder")
226
+ .icon(pageFolderIcon)
227
+ .intent({
228
+ type: "create",
229
+ params: {
230
+ type: "pageFolder",
231
+ template: "pageFolder",
232
+ },
233
+ }),
234
+ ]
235
+ : [
236
+ S.menuItem()
237
+ .title("Create Page")
238
+ .icon(pageIcon)
239
+ .intent({
240
+ type: "create",
241
+ params: [
242
+ {
243
+ type: "page",
244
+ template: "page-with-folder",
245
+ },
246
+ {
247
+ pageFolderId: currentFolderId,
248
+ },
249
+ ],
250
+ }),
251
+ S.menuItem()
252
+ .title("Create Folder")
253
+ .icon(pageFolderIcon)
254
+ .intent({
255
+ type: "create",
256
+ params: [
257
+ {
258
+ type: "pageFolder",
259
+ template: "page-folder-with-parent",
260
+ },
261
+ {
262
+ parentFolderId: currentFolderId,
263
+ },
264
+ ],
265
+ }),
266
+ ]
267
+
268
+ const folderMenuItems =
269
+ currentFolderId === null
270
+ ? []
271
+ : [
272
+ S.menuItem()
273
+ .title("Folder Settings")
274
+ .icon(pageFolderIcon)
275
+ .intent({
276
+ type: "edit",
277
+ params: {
278
+ id: currentFolderId,
279
+ type: "pageFolder",
280
+ },
281
+ }),
282
+ ]
283
+
284
+ return S.list()
285
+ .title(title)
286
+ .canHandleIntent((intentName, params) => {
287
+ if (intentName === "create") {
288
+ return (
289
+ typeof params.template === "string" &&
290
+ allowedTemplateIds.includes(params.template)
291
+ )
292
+ }
293
+
294
+ if (intentName !== "edit") {
295
+ return false
296
+ }
297
+
298
+ const normalizedId =
299
+ typeof params.id === "string" ? normalizeDocumentId(params.id) : null
300
+
301
+ if (!normalizedId || typeof params.type !== "string") {
302
+ return false
303
+ }
304
+
305
+ if (
306
+ params.type === "pageFolder" &&
307
+ currentFolderId !== null &&
308
+ normalizedId === normalizeDocumentId(currentFolderId)
309
+ ) {
310
+ return true
311
+ }
312
+
313
+ return sortedItems.some(
314
+ (item) =>
315
+ normalizeDocumentId(item._id) === normalizedId &&
316
+ item._type === params.type
317
+ )
318
+ })
319
+ .child((itemId, options) => {
320
+ const normalizedId = getDocumentIdFromPageTreeItemId(itemId)
321
+ const matchedItem = sortedItems.find(
322
+ (item) => normalizeDocumentId(item._id) === normalizedId
323
+ )
324
+
325
+ if (matchedItem?._type === "pageFolder") {
326
+ return buildPageTreeList(
327
+ S,
328
+ context,
329
+ matchedItem._id,
330
+ matchedItem.title || "Untitled Folder"
331
+ )
332
+ }
333
+
334
+ if (matchedItem?._type === "page") {
335
+ return S.document().schemaType("page").documentId(normalizedId)
336
+ }
337
+
338
+ if (
339
+ currentFolderId !== null &&
340
+ normalizedId === normalizeDocumentId(currentFolderId)
341
+ ) {
342
+ return S.document().schemaType("pageFolder").documentId(normalizedId)
343
+ }
344
+
345
+ const templateId =
346
+ typeof options.params?.template === "string"
347
+ ? options.params.template
348
+ : null
349
+
350
+ if (!templateId) {
351
+ return undefined
352
+ }
353
+
354
+ return buildPageTreeCreateChild(
355
+ S,
356
+ normalizedId,
357
+ currentFolderId,
358
+ templateId
359
+ )
360
+ })
361
+ .menuItems([...folderMenuItems, ...createMenuItems])
362
+ .initialValueTemplates(initialValueTemplates)
363
+ .items(childItems)
364
+ })
365
+ }
366
+
367
+ // https://www.sanity.io/docs/structure-builder-cheat-sheet
368
+ export const structure: StructureResolver = (S, context) =>
369
+ S.list()
370
+ .title("Content")
371
+ .items([
372
+ // GROUP 1: Pages -- root page tree with page/folder creation
373
+ S.listItem()
374
+ .title("Pages")
375
+ .icon(pagesFolderIcon)
376
+ .child(() => buildPageTreeList(S, context, null, "Pages")),
377
+
378
+ S.divider(),
379
+
380
+ // GROUP 2: Content -- authors, blog categories
381
+ S.listItem()
382
+ .title("Content")
383
+ .icon(contentFolderIcon)
384
+ .child(
385
+ S.list()
386
+ .title("Content")
387
+ .items([
388
+ S.documentTypeListItem("author")
389
+ .title("Authors")
390
+ .icon(authorIcon),
391
+ S.documentTypeListItem("blogCategory")
392
+ .title("Blog Categories")
393
+ .icon(blogCategoryIcon),
394
+ ])
395
+ ),
396
+
397
+ S.divider(),
398
+
399
+ // GROUP 3: Components -- singleton and reusable components
400
+ S.listItem()
401
+ .title("Components")
402
+ .icon(componentsFolderIcon)
403
+ .child(
404
+ S.list()
405
+ .title("Components")
406
+ .items([
407
+ S.divider().title("Singleton Components"),
408
+ ...singletonComponents.map((item) =>
409
+ S.listItem()
410
+ .title(item.title)
411
+ .id(item.documentId)
412
+ .icon(item.icon)
413
+ .child(
414
+ S.document()
415
+ .schemaType(item.schemaType)
416
+ .documentId(item.documentId)
417
+ )
418
+ ),
419
+ S.divider().title("Reusable Components"),
420
+ S.documentTypeListItem("hero").title("Headings").icon(heroIcon),
421
+ S.documentTypeListItem("description")
422
+ .title("Descriptions")
423
+ .icon(descriptionIcon),
424
+ S.documentTypeListItem("blogContent")
425
+ .title("Blog Content")
426
+ .icon(contentIcon),
427
+ ])
428
+ ),
429
+
430
+ S.divider(),
431
+
432
+ // GROUP 4: Layout -- global layout singletons
433
+ S.listItem()
434
+ .title("Layout")
435
+ .icon(layoutFolderIcon)
436
+ .child(
437
+ S.list()
438
+ .title("Layout")
439
+ .items([
440
+ ...singletonLayout.map((item) =>
441
+ S.listItem()
442
+ .title(item.title)
443
+ .id(item.documentId)
444
+ .icon(item.icon)
445
+ .child(
446
+ S.document()
447
+ .schemaType(item.schemaType)
448
+ .documentId(item.documentId)
449
+ )
450
+ ),
451
+ ])
452
+ ),
453
+ ])
@@ -0,0 +1,8 @@
1
+ import { createImageUrlBuilder } from "@sanity/image-url"
2
+ import { dataset, projectId } from "../env"
3
+
4
+ const builder = createImageUrlBuilder({ projectId, dataset })
5
+
6
+ export const urlForImage = (source: Parameters<typeof builder.image>[0]) => {
7
+ return builder.image(source)
8
+ }
@@ -0,0 +1,137 @@
1
+ type PageReference = {
2
+ _id?: string
3
+ _ref?: string
4
+ _type?: string
5
+ slug?: { current?: string | null } | null
6
+ resolvedSlug?: string | null
7
+ title?: string | null
8
+ }
9
+
10
+ type PageReferenceLink = {
11
+ _type: "pageReference"
12
+ page?: PageReference | null
13
+ text?: string | null
14
+ openInNewTab?: boolean | null
15
+ }
16
+
17
+ type ExternalLinkItem = {
18
+ _type: "externalLink"
19
+ href?: string | null
20
+ text?: string | null
21
+ openInNewTab?: boolean | null
22
+ }
23
+
24
+ type LinkItem = PageReferenceLink | ExternalLinkItem
25
+
26
+ // Backward-compatible shape (old data without _type)
27
+ type LegacyLink = {
28
+ page?: PageReference | null
29
+ href?: string | null
30
+ text?: string | null
31
+ openInNewTab?: boolean | null
32
+ }
33
+
34
+ type LinkLike = LinkItem | LegacyLink
35
+
36
+ type InternalReference = {
37
+ _id?: string
38
+ _type: string
39
+ slug?: { current?: string | null } | null
40
+ resolvedSlug?: string | null
41
+ title?: string
42
+ }
43
+
44
+ /**
45
+ * Resolves a link object to a URL string.
46
+ *
47
+ * Supports both the new discriminated union shape (_type: "pageReference" | "externalLink")
48
+ * and the legacy flat shape (page + href without _type).
49
+ */
50
+ export const urlForReference = (link: LinkLike | null): string => {
51
+ if (!link) return "#"
52
+
53
+ // New discriminated union
54
+ if ("_type" in link && link._type === "pageReference") {
55
+ if (
56
+ link.page &&
57
+ typeof link.page === "object" &&
58
+ ("slug" in link.page || "_id" in link.page)
59
+ ) {
60
+ const ref = link.page as InternalReference
61
+ return resolveDocumentUrl(
62
+ ref._type,
63
+ ref.resolvedSlug ?? undefined,
64
+ ref.slug?.current ?? undefined,
65
+ ref._id
66
+ )
67
+ }
68
+ return "#"
69
+ }
70
+
71
+ if ("_type" in link && link._type === "externalLink") {
72
+ return link.href || "#"
73
+ }
74
+
75
+ // Legacy fallback (flat shape without _type)
76
+ if (
77
+ "page" in link &&
78
+ link.page &&
79
+ typeof link.page === "object" &&
80
+ ("slug" in link.page || "_id" in link.page)
81
+ ) {
82
+ const ref = link.page as InternalReference
83
+ return resolveDocumentUrl(
84
+ ref._type,
85
+ ref.resolvedSlug ?? undefined,
86
+ ref.slug?.current ?? undefined,
87
+ ref._id
88
+ )
89
+ }
90
+
91
+ if ("href" in link && link.href) {
92
+ return link.href
93
+ }
94
+
95
+ return "#"
96
+ }
97
+
98
+ function resolveDocumentUrl(
99
+ documentType: string,
100
+ resolvedSlug?: string,
101
+ slug?: string,
102
+ documentId?: string
103
+ ): string {
104
+ if (documentType === "page") {
105
+ if (documentId === "page-homepage") return "/"
106
+ if (resolvedSlug) return `/${resolvedSlug}`
107
+ if (slug) return `/${slug}`
108
+ return "#"
109
+ }
110
+
111
+ if (!slug) return "#"
112
+
113
+ console.warn("Unknown document type for URL resolution:", documentType)
114
+ return "#"
115
+ }
116
+
117
+ /**
118
+ * Returns href, target, and rel attributes for a link object.
119
+ *
120
+ * External detection: externalLink type, or URLs starting with "http".
121
+ */
122
+ export const getLinkAttributes = (link: LinkLike | null) => {
123
+ if (!link) return { href: "#", target: undefined, rel: undefined }
124
+
125
+ const href = urlForReference(link)
126
+ const isExternal =
127
+ ("_type" in link && link._type === "externalLink") ||
128
+ href.startsWith("http")
129
+
130
+ const shouldOpenNewTab = link.openInNewTab || isExternal
131
+
132
+ return {
133
+ href,
134
+ target: shouldOpenNewTab ? "_blank" : undefined,
135
+ rel: shouldOpenNewTab ? "noopener noreferrer" : undefined,
136
+ }
137
+ }
@@ -0,0 +1,81 @@
1
+ import { portableTextToMarkdown } from "@portabletext/markdown"
2
+ import type { PAGE_QUERY_RESULT } from "../sanity.types"
3
+
4
+ const markdownOptions = {
5
+ types: {
6
+ table: ({ value }: { value: { rows?: Array<{ cells?: string[] }> } }) => {
7
+ const rows = value?.rows
8
+ if (!rows?.length) return ""
9
+ return rows
10
+ .map((row) => `| ${(row.cells ?? []).join(" | ")} |`)
11
+ .join("\n")
12
+ },
13
+ },
14
+ } satisfies Parameters<typeof portableTextToMarkdown>[1]
15
+
16
+ type PageBuilderBlock = NonNullable<
17
+ NonNullable<PAGE_QUERY_RESULT>["pageBuilder"]
18
+ >[number]
19
+
20
+ export const renderPageBuilderMarkdown = (
21
+ blocks: PageBuilderBlock[] | null | undefined
22
+ ) => {
23
+ if (!blocks?.length) return ""
24
+
25
+ return blocks
26
+ .flatMap((block) => {
27
+ switch (block._type) {
28
+ case "hero":
29
+ return block.headline ? [`## ${block.headline}`, ""] : []
30
+ case "description":
31
+ return block.description ? [block.description, ""] : []
32
+ case "blogContent": {
33
+ const lines: (string | null)[] = []
34
+ const categories =
35
+ "categories" in block ? block.categories : undefined
36
+ const relatedPosts =
37
+ "relatedPosts" in block ? block.relatedPosts : undefined
38
+
39
+ if (categories?.length) {
40
+ lines.push(
41
+ categories
42
+ .map((c) => c.title)
43
+ .filter(Boolean)
44
+ .join(", ")
45
+ )
46
+ }
47
+ if (block.author?.name) {
48
+ const authorParts = [block.author.name, block.author.role]
49
+ .filter(Boolean)
50
+ .join(", ")
51
+ lines.push(`**Author:** ${authorParts}`)
52
+ }
53
+ if (block.date) {
54
+ lines.push(`**Date:** ${block.date}`)
55
+ }
56
+ lines.push("")
57
+ if (block.body) {
58
+ lines.push(
59
+ portableTextToMarkdown(
60
+ block.body as Parameters<typeof portableTextToMarkdown>[0],
61
+ markdownOptions
62
+ )
63
+ )
64
+ }
65
+ if (relatedPosts?.length) {
66
+ lines.push("", "## Related Articles", "")
67
+ for (const post of relatedPosts) {
68
+ if (post.title && post.resolvedSlug) {
69
+ lines.push(`- [${post.title}](/${post.resolvedSlug}.md)`)
70
+ }
71
+ }
72
+ }
73
+ lines.push("")
74
+ return lines.filter((l) => l !== null)
75
+ }
76
+ default:
77
+ return []
78
+ }
79
+ })
80
+ .join("\n")
81
+ }