bsmnt 0.13.1 → 0.15.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 (177) hide show
  1. package/dist/core/create/internal-ids.d.ts +1 -0
  2. package/dist/core/create/internal-ids.d.ts.map +1 -1
  3. package/dist/core/create/internal-ids.js +1 -0
  4. package/dist/core/create/internal-ids.js.map +1 -1
  5. package/dist/domain/skills.d.ts +5 -0
  6. package/dist/domain/skills.d.ts.map +1 -1
  7. package/dist/domain/skills.js +9 -1
  8. package/dist/domain/skills.js.map +1 -1
  9. package/dist/infrastructure/create/format-project.d.ts +9 -0
  10. package/dist/infrastructure/create/format-project.d.ts.map +1 -0
  11. package/dist/infrastructure/create/format-project.js +21 -0
  12. package/dist/infrastructure/create/format-project.js.map +1 -0
  13. package/dist/infrastructure/create/lifecycle-registry.d.ts.map +1 -1
  14. package/dist/infrastructure/create/lifecycle-registry.js +5 -0
  15. package/dist/infrastructure/create/lifecycle-registry.js.map +1 -1
  16. package/dist/modules/features/cms/sanity-pagebuilder/config.d.ts.map +1 -1
  17. package/dist/modules/features/cms/sanity-pagebuilder/config.js +1 -0
  18. package/dist/modules/features/cms/sanity-pagebuilder/config.js.map +1 -1
  19. package/dist/modules/features/cms/sanity-pagebuilder/index.d.ts.map +1 -1
  20. package/dist/modules/features/cms/sanity-pagebuilder/index.js +1 -0
  21. package/dist/modules/features/cms/sanity-pagebuilder/index.js.map +1 -1
  22. package/dist/modules/features/cms/sanity-pagebuilder-cache/config.d.ts.map +1 -1
  23. package/dist/modules/features/cms/sanity-pagebuilder-cache/config.js +1 -0
  24. package/dist/modules/features/cms/sanity-pagebuilder-cache/config.js.map +1 -1
  25. package/dist/modules/features/cms/sanity-pagebuilder-cache/index.d.ts.map +1 -1
  26. package/dist/modules/features/cms/sanity-pagebuilder-cache/index.js +1 -0
  27. package/dist/modules/features/cms/sanity-pagebuilder-cache/index.js.map +1 -1
  28. package/package.json +1 -1
  29. package/src/agent-skills/ashby-pagebuilder-block/SKILL.md +29 -11
  30. package/src/agent-skills/hubspot-pagebuilder-block/SKILL.md +31 -12
  31. package/src/agent-skills/launch-ready/SKILL.md +114 -0
  32. package/src/agent-skills/launch-ready/references/a11y.md +74 -0
  33. package/src/agent-skills/launch-ready/references/content.md +65 -0
  34. package/src/agent-skills/launch-ready/references/crawlers.md +89 -0
  35. package/src/agent-skills/launch-ready/references/launch.md +44 -0
  36. package/src/agent-skills/launch-ready/references/metadata.md +83 -0
  37. package/src/agent-skills/launch-ready/references/performance.md +36 -0
  38. package/src/agent-skills/launch-ready/references/routing.md +47 -0
  39. package/src/agent-skills/launch-ready/references/security.md +39 -0
  40. package/src/agent-skills/page-builder-block/SKILL.md +26 -13
  41. package/src/modules/features/cms/sanity/files/app/layout.tsx +2 -2
  42. package/src/modules/features/cms/sanity/files/lib/utils/metadata.ts +2 -2
  43. package/src/modules/features/cms/sanity/files/lib/utils/url.ts +6 -3
  44. package/src/modules/features/cms/sanity-pagebuilder/files/app/api/revalidate/route.ts +2 -2
  45. package/src/modules/features/cms/sanity-pagebuilder/files/components/sanity/rich-text.tsx +4 -2
  46. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/block-actions-item.tsx +2 -2
  47. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/component-preview-frame.tsx +124 -0
  48. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/component-preview-pane.tsx +25 -0
  49. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/page-builder-input.tsx +3 -26
  50. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/page-builder-insert-menu.tsx +121 -93
  51. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/redirect-import-dialog.tsx +538 -0
  52. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/csv.ts +88 -0
  53. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/fetchers/breadcrumbs.ts +22 -0
  54. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/icons.ts +2 -2
  55. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/page-builder-config.ts +36 -49
  56. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/presentation.ts +1 -50
  57. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/queries.ts +76 -64
  58. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/redirect-import.ts +651 -0
  59. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/sanity.config.ts +6 -8
  60. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/index.ts +0 -1
  61. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/index.ts +1 -4
  62. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/redirect.ts +9 -6
  63. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/structured-data.ts +8 -0
  64. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/block.ts +4 -4
  65. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/metadata.ts +60 -6
  66. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/page-builder.ts +4 -18
  67. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/page.ts +33 -38
  68. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/richText.ts +0 -1
  69. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/singletons.ts +0 -16
  70. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/structure.ts +52 -35
  71. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/tags.ts +5 -2
  72. package/src/modules/features/cms/sanity-pagebuilder/files/lib/scripts/sanity-typegen.ts +8 -11
  73. package/src/modules/features/cms/sanity-pagebuilder/files/lib/utils/metadata.ts +109 -136
  74. package/src/modules/features/cms/sanity-pagebuilder/files/lib/utils/studio-embed.ts +22 -0
  75. package/src/modules/features/cms/sanity-pagebuilder-cache/files/app/api/revalidate/route.ts +2 -2
  76. package/src/modules/features/cms/sanity-pagebuilder-cache/files/components/sanity/rich-text.tsx +4 -2
  77. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/block-actions-item.tsx +2 -2
  78. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/component-preview-frame.tsx +124 -0
  79. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/component-preview-pane.tsx +25 -0
  80. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/page-builder-input.tsx +3 -26
  81. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/page-builder-insert-menu.tsx +121 -93
  82. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/redirect-import-dialog.tsx +538 -0
  83. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/csv.ts +88 -0
  84. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/fetchers/breadcrumbs.ts +25 -0
  85. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/icons.ts +2 -2
  86. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/page-builder-config.ts +36 -49
  87. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/presentation.ts +1 -50
  88. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/queries.ts +76 -64
  89. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/redirect-import.ts +651 -0
  90. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/sanity.config.ts +6 -8
  91. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/index.ts +0 -1
  92. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/index.ts +1 -4
  93. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/redirect.ts +9 -6
  94. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/layout/structured-data.ts +8 -0
  95. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/block.ts +4 -4
  96. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/metadata.ts +60 -6
  97. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/page-builder.ts +4 -18
  98. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/page.ts +33 -38
  99. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/shared/richText.ts +0 -1
  100. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/singletons.ts +0 -16
  101. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/structure.ts +52 -35
  102. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/tags.ts +6 -2
  103. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/scripts/sanity-typegen.ts +8 -11
  104. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/utils/metadata.ts +109 -136
  105. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/utils/studio-embed.ts +22 -0
  106. package/src/modules/features/forms/hubspot/files/_sanity-adapter/form-block.ts +4 -10
  107. package/src/modules/features/forms/hubspot/files/lib/integrations/hubspot/README.md +10 -5
  108. package/src/modules/features/jobs/ashby/files/_sanity-adapter/job-board-block.ts +0 -7
  109. package/src/modules/features/jobs/ashby/files/lib/integrations/ashby/README.md +11 -5
  110. package/src/templates/next-default/.env.example +3 -1
  111. package/src/templates/next-default/app/layout.tsx +2 -1
  112. package/src/templates/next-default/app/robots.ts +9 -3
  113. package/src/templates/next-default/lib/utils/metadata.ts +2 -1
  114. package/src/templates/next-default/lib/utils/url.ts +20 -3
  115. package/src/templates/next-default/next.config.ts +16 -0
  116. package/src/templates/next-experiments/.env.example +3 -1
  117. package/src/templates/next-experiments/app/layout.tsx +2 -1
  118. package/src/templates/next-experiments/app/robots.ts +9 -3
  119. package/src/templates/next-experiments/lib/utils/metadata.ts +2 -1
  120. package/src/templates/next-experiments/lib/utils/url.ts +20 -3
  121. package/src/templates/next-experiments/next.config.ts +16 -0
  122. package/src/templates/next-pagebuilder/.env.example +3 -1
  123. package/src/templates/next-pagebuilder/.github/workflows/ci.yml +32 -8
  124. package/src/templates/next-pagebuilder/AGENTS.md +22 -7
  125. package/src/templates/next-pagebuilder/README.md +15 -3
  126. package/src/templates/next-pagebuilder/app/(content)/[[...slug]]/page.tsx +45 -7
  127. package/src/templates/next-pagebuilder/app/actions/unlock-gated-content.ts +5 -4
  128. package/src/templates/next-pagebuilder/app/components/page.tsx +81 -0
  129. package/src/templates/next-pagebuilder/app/layout.tsx +3 -41
  130. package/src/templates/next-pagebuilder/app/robots.ts +9 -3
  131. package/src/templates/next-pagebuilder/biome.json +2 -1
  132. package/src/templates/next-pagebuilder/components/layout/footer/index.tsx +1 -1
  133. package/src/templates/next-pagebuilder/components/page-builder/components/gated-content/index.tsx +8 -3
  134. package/src/templates/next-pagebuilder/components/page-builder/library.ts +183 -0
  135. package/src/templates/next-pagebuilder/components/page-builder/registry.tsx +4 -1
  136. package/src/templates/next-pagebuilder/components/page-builder/renderer.tsx +3 -1
  137. package/src/templates/next-pagebuilder/components/page-builder/stage.tsx +10 -0
  138. package/src/templates/next-pagebuilder/components/page-document/index.tsx +28 -35
  139. package/src/templates/next-pagebuilder/lefthook.yml +1 -1
  140. package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.types.ts +170 -299
  141. package/src/templates/next-pagebuilder/lib/utils/base-url.ts +18 -3
  142. package/src/templates/next-pagebuilder/lib/utils/breadcrumbs.ts +70 -0
  143. package/src/templates/next-pagebuilder/next.config.ts +16 -0
  144. package/src/templates/next-pagebuilder/package.json +1 -1
  145. package/src/templates/next-pagebuilder-cache/.env.example +3 -1
  146. package/src/templates/next-pagebuilder-cache/.github/workflows/ci.yml +32 -8
  147. package/src/templates/next-pagebuilder-cache/AGENTS.md +22 -7
  148. package/src/templates/next-pagebuilder-cache/README.md +15 -3
  149. package/src/templates/next-pagebuilder-cache/app/(content)/[[...slug]]/page.tsx +46 -7
  150. package/src/templates/next-pagebuilder-cache/app/actions/unlock-gated-content.ts +5 -4
  151. package/src/templates/next-pagebuilder-cache/app/components/page.tsx +91 -0
  152. package/src/templates/next-pagebuilder-cache/app/layout.tsx +5 -46
  153. package/src/templates/next-pagebuilder-cache/app/robots.ts +9 -3
  154. package/src/templates/next-pagebuilder-cache/biome.json +2 -1
  155. package/src/templates/next-pagebuilder-cache/components/layout/footer/index.tsx +1 -1
  156. package/src/templates/next-pagebuilder-cache/components/page-builder/components/gated-content/index.tsx +8 -3
  157. package/src/templates/next-pagebuilder-cache/components/page-builder/library.ts +183 -0
  158. package/src/templates/next-pagebuilder-cache/components/page-builder/registry.tsx +4 -1
  159. package/src/templates/next-pagebuilder-cache/components/page-builder/renderer.tsx +3 -1
  160. package/src/templates/next-pagebuilder-cache/components/page-builder/stage.tsx +10 -0
  161. package/src/templates/next-pagebuilder-cache/components/page-document/index.tsx +28 -35
  162. package/src/templates/next-pagebuilder-cache/lefthook.yml +1 -1
  163. package/src/templates/next-pagebuilder-cache/lib/integrations/sanity/sanity.types.ts +170 -299
  164. package/src/templates/next-pagebuilder-cache/lib/utils/base-url.ts +18 -3
  165. package/src/templates/next-pagebuilder-cache/lib/utils/breadcrumbs.ts +70 -0
  166. package/src/templates/next-pagebuilder-cache/next.config.ts +16 -0
  167. package/src/templates/next-pagebuilder-cache/package.json +1 -1
  168. package/src/templates/next-webgl/.env.example +3 -1
  169. package/src/templates/next-webgl/app/layout.tsx +2 -1
  170. package/src/templates/next-webgl/app/robots.ts +9 -3
  171. package/src/templates/next-webgl/lib/utils/metadata.ts +2 -1
  172. package/src/templates/next-webgl/lib/utils/url.ts +20 -3
  173. package/src/templates/next-webgl/next.config.ts +16 -0
  174. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/use-studio-preview-urls.ts +0 -62
  175. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/components/studio-preview.ts +0 -37
  176. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/components/use-studio-preview-urls.ts +0 -62
  177. package/src/modules/features/cms/sanity-pagebuilder-cache/files/lib/integrations/sanity/schemas/components/studio-preview.ts +0 -37
@@ -1,36 +1,13 @@
1
1
  import type { Metadata } from "next";
2
+ import { stegaClean } from "next-sanity";
2
3
  import { urlForImage } from "@/lib/integrations/sanity/utils/image";
3
4
  import { getBaseUrl } from "./base-url";
4
5
 
5
- interface GenerateMetadataOptions {
6
- title?: string | undefined;
7
- description?: string | undefined;
8
- keywords?: string[] | undefined;
9
- ogTitle?: string | undefined;
10
- ogDescription?: string | undefined;
11
- canonicalUrl?: string | undefined;
12
- image?:
13
- | {
14
- url?: string | undefined;
15
- width?: number | undefined;
16
- height?: number | undefined;
17
- }
18
- | undefined;
19
- url?: string | undefined;
20
- siteName?: string | undefined;
21
- index?: boolean | undefined;
22
- noIndex?: boolean | undefined;
23
- type?: "website" | "article";
24
- publishedTime?: string | undefined;
25
- modifiedTime?: string | undefined;
26
- authors?: string[] | undefined;
27
- }
28
-
29
6
  interface SanityMetadataImage {
30
7
  asset?: {
31
8
  _ref?: string;
32
9
  _type?: "reference";
33
- };
10
+ } | null;
34
11
  }
35
12
 
36
13
  interface SanityPageMetadata {
@@ -44,6 +21,7 @@ interface SanityPageMetadata {
44
21
  image?: SanityMetadataImage | null;
45
22
  } | null;
46
23
  index?: boolean | null;
24
+ follow?: boolean | null;
47
25
  title?: string | null;
48
26
  description?: string | null;
49
27
  image?: SanityMetadataImage | null;
@@ -52,138 +30,133 @@ interface SanityPageMetadata {
52
30
 
53
31
  interface SanityMetadataDocument {
54
32
  title?: string | null;
55
- subtitle?: string | null;
56
33
  metadata?: SanityPageMetadata | null;
57
34
  }
58
35
 
36
+ // The Structured Data singleton: its name is the og:site_name and its share
37
+ // image covers any page without an OG image of its own.
38
+ interface SiteMetadata {
39
+ name?: string | null;
40
+ shareImage?: SanityMetadataImage | null;
41
+ }
42
+
43
+ interface ArticleMetadata {
44
+ publishedTime?: string | null | undefined;
45
+ modifiedTime?: string | null | undefined;
46
+ authors?: readonly (string | null | undefined)[] | null | undefined;
47
+ }
48
+
59
49
  interface GenerateSanityMetadataOptions {
60
50
  document: SanityMetadataDocument;
61
51
  url?: string;
62
- siteName?: string;
63
- type?: "website" | "article";
52
+ site?: SiteMetadata | null | undefined;
53
+ fallbackDescription?: string | null | undefined;
54
+ fallbackImage?: SanityMetadataImage | null | undefined;
55
+ article?: ArticleMetadata | undefined;
64
56
  }
65
57
 
58
+ const OG_WIDTH = 1200;
59
+ const OG_HEIGHT = 630;
60
+ const DESCRIPTION_LENGTH = 160;
61
+
66
62
  const APP_BASE_URL = getBaseUrl();
67
63
 
68
- /**
69
- * Generate complete metadata object for pages
70
- *
71
- * @example
72
- * ```ts
73
- * export async function generateMetadata({ params }) {
74
- * const page = await fetchPage(params.slug)
75
- *
76
- * return generatePageMetadata({
77
- * title: page.metadata?.title || page.title,
78
- * description: page.metadata?.description,
79
- * image: { url: page.metadata?.image?.asset?.url },
80
- * url: `/page/${params.slug}`,
81
- * noIndex: page.metadata?.noIndex,
82
- * })
83
- * }
84
- * ```
85
- */
86
- const generatePageMetadata = (options: GenerateMetadataOptions): Metadata => {
87
- const {
88
- title,
89
- description,
90
- keywords,
91
- ogTitle,
92
- ogDescription,
93
- canonicalUrl,
94
- image,
95
- url,
96
- siteName = "Basement",
97
- index,
98
- noIndex = false,
99
- type = "website",
100
- publishedTime,
101
- modifiedTime,
102
- authors,
103
- } = options;
104
-
105
- const shouldIndex = index ?? !noIndex;
106
- const fullUrl = url ? `${APP_BASE_URL}${url}` : APP_BASE_URL;
107
- const imageUrl = image?.url || "/opengraph-image.jpg";
108
- const width = image?.width || 1200;
109
- const height = image?.height || 630;
110
-
111
- // Social title/description fall back to the SEO meta title/description.
112
- const socialTitle = ogTitle || title;
113
- const socialDescription = ogDescription || description;
114
-
115
- const metadata: Metadata = {
64
+ // A relative override is rooted, so "blog/post" can't resolve against the
65
+ // current path.
66
+ const resolveCanonical = (
67
+ override: string | null | undefined,
68
+ ownPath: string,
69
+ ) => {
70
+ const value = override?.trim();
71
+ if (!value) return ownPath;
72
+ if (/^https?:\/\//i.test(value)) return value;
73
+ return `/${value.replace(/^\/+/, "")}`;
74
+ };
75
+
76
+ // Trims body copy to a meta description, cut at a word boundary.
77
+ const summarize = (
78
+ text: string | null | undefined,
79
+ maxLength = DESCRIPTION_LENGTH,
80
+ ) => {
81
+ const clean = text?.replace(/\s+/g, " ").trim();
82
+ if (!clean) return undefined;
83
+ if (clean.length <= maxLength) return clean;
84
+ const cut = clean.slice(0, maxLength - 1);
85
+ const lastSpace = cut.lastIndexOf(" ");
86
+ return `${(lastSpace > 0 ? cut.slice(0, lastSpace) : cut).replace(/[\s,;:.–—-]+$/, "")}…`;
87
+ };
88
+
89
+ const socialImage = (source: SanityMetadataImage | null | undefined) =>
90
+ source?.asset
91
+ ? urlForImage(source).width(OG_WIDTH).height(OG_HEIGHT).url()
92
+ : undefined;
93
+
94
+ export const generateSanityMetadata = ({
95
+ document,
96
+ url = "/",
97
+ site,
98
+ fallbackDescription,
99
+ fallbackImage,
100
+ article,
101
+ }: GenerateSanityMetadataOptions): Metadata => {
102
+ const { name, shareImage } = stegaClean(site) ?? {};
103
+ const siteName = name?.trim() || undefined;
104
+
105
+ const seo = document.metadata;
106
+ const title = seo?.metaTitle || seo?.title || document.title || siteName;
107
+ const description =
108
+ seo?.metaDescription || seo?.description || summarize(fallbackDescription);
109
+ const socialTitle = seo?.ogTitle || title;
110
+ const socialDescription = seo?.ogDescription || description;
111
+ const imageUrl =
112
+ socialImage(seo?.image) ||
113
+ socialImage(seo?.og?.image) ||
114
+ socialImage(fallbackImage) ||
115
+ socialImage(shareImage);
116
+ const images = imageUrl
117
+ ? [{ url: imageUrl, width: OG_WIDTH, height: OG_HEIGHT }]
118
+ : undefined;
119
+
120
+ const canonical = resolveCanonical(seo?.canonicalUrl, url);
121
+
122
+ const index = seo?.index ?? (seo?.noIndex != null ? !seo.noIndex : true);
123
+ // Unset on pages that predate the toggle, which were `nofollow` whenever
124
+ // they were `noindex`.
125
+ const follow = seo?.follow ?? index;
126
+ const authors = article?.authors?.filter((author): author is string =>
127
+ Boolean(author),
128
+ );
129
+
130
+ return {
116
131
  metadataBase: new URL(APP_BASE_URL),
117
132
  title,
118
133
  description,
119
- keywords,
120
- alternates: { canonical: canonicalUrl || url || "/" },
134
+ keywords: seo?.keywords?.length ? seo.keywords : undefined,
135
+ alternates: { canonical },
121
136
  openGraph: {
137
+ title: socialTitle,
122
138
  description: socialDescription,
123
- url: fullUrl,
139
+ // Follows the canonical, so an override doesn't leave og:url pointing
140
+ // at the duplicate.
141
+ url: new URL(canonical, APP_BASE_URL).toString(),
124
142
  siteName,
125
143
  locale: "en_US",
126
- type,
127
- images: [{ url: imageUrl, width, height }],
128
- ...(socialTitle && { title: socialTitle }),
129
- ...(publishedTime && { publishedTime }),
130
- ...(modifiedTime && { modifiedTime }),
131
- ...(authors && { authors }),
144
+ images,
145
+ ...(article
146
+ ? {
147
+ type: "article",
148
+ publishedTime: article.publishedTime || undefined,
149
+ modifiedTime: article.modifiedTime || undefined,
150
+ authors: authors?.length ? authors : undefined,
151
+ }
152
+ : { type: "website" }),
132
153
  },
133
154
  twitter: {
134
155
  card: "summary_large_image",
156
+ title: socialTitle,
135
157
  description: socialDescription,
136
- images: [{ url: imageUrl, width, height }],
137
- ...(socialTitle && { title: socialTitle }),
158
+ images,
138
159
  },
139
- other: { "fb:app_id": process.env.NEXT_PUBLIC_FACEBOOK_APP_ID || "" },
160
+ ...(index && follow ? {} : { robots: { index, follow } }),
140
161
  };
141
-
142
- if (!shouldIndex) metadata.robots = { index: false, follow: false };
143
-
144
- return metadata;
145
- };
146
-
147
- export const generateSanityMetadata = ({
148
- document,
149
- url,
150
- siteName,
151
- type = "website",
152
- }: GenerateSanityMetadataOptions): Metadata => {
153
- const pageMetadata = document.metadata;
154
- const title =
155
- pageMetadata?.metaTitle ||
156
- pageMetadata?.title ||
157
- document.title ||
158
- undefined;
159
- const description =
160
- pageMetadata?.metaDescription ||
161
- pageMetadata?.description ||
162
- document.subtitle ||
163
- undefined;
164
- const socialImageSource = pageMetadata?.image || pageMetadata?.og?.image;
165
- const imageUrl = socialImageSource
166
- ? urlForImage(socialImageSource).width(1200).height(630).url()
167
- : undefined;
168
- const index =
169
- pageMetadata?.index ??
170
- (pageMetadata?.noIndex != null ? !pageMetadata.noIndex : true);
171
-
172
- const keywords = pageMetadata?.keywords?.length
173
- ? pageMetadata.keywords
174
- : undefined;
175
-
176
- return generatePageMetadata({
177
- title,
178
- description,
179
- keywords,
180
- ogTitle: pageMetadata?.ogTitle || undefined,
181
- ogDescription: pageMetadata?.ogDescription || undefined,
182
- canonicalUrl: pageMetadata?.canonicalUrl || undefined,
183
- image: imageUrl ? { url: imageUrl, width: 1200, height: 630 } : undefined,
184
- url,
185
- siteName,
186
- index,
187
- type,
188
- });
189
162
  };
@@ -0,0 +1,22 @@
1
+ import { headers } from "next/headers";
2
+
3
+ const STUDIO_PATH = "/studio";
4
+
5
+ const isStudioReferer = (referer: string | null) => {
6
+ if (!referer) return false;
7
+
8
+ try {
9
+ return new URL(referer).pathname.startsWith(STUDIO_PATH);
10
+ } catch {
11
+ return false;
12
+ }
13
+ };
14
+
15
+ export const isStudioEmbed = async () => {
16
+ const headerList = await headers();
17
+
18
+ return (
19
+ headerList.get("sec-fetch-dest") === "iframe" ||
20
+ isStudioReferer(headerList.get("referer"))
21
+ );
22
+ };
@@ -4,7 +4,7 @@ import { parseBody } from "next-sanity/webhook";
4
4
  // `directClient`, never `client`: a CDN hit returns the pre-mutation slug, which
5
5
  // purges the old path twice and never the new one.
6
6
  import { directClient } from "@/lib/integrations/sanity/client";
7
- import { pageBuilderReferenceMembers as pbrm } from "@/lib/integrations/sanity/page-builder-config";
7
+ import { pageBuilderBlockMembers } from "@/lib/integrations/sanity/page-builder-config";
8
8
  import {
9
9
  ALL_PAGE_SLUGS_QUERY,
10
10
  FOLDER_PATH_QUERY,
@@ -64,7 +64,7 @@ type RevalidateWebhookBody = WebhookDocument & {
64
64
  };
65
65
 
66
66
  const referenceableTypes = new Set<string>([
67
- ...pbrm.map((m) => m.documentType),
67
+ ...pageBuilderBlockMembers.map((member) => member.documentType),
68
68
  ...REFERENCEABLE_TYPES,
69
69
  ]);
70
70
 
@@ -4,7 +4,7 @@ import { SanityImage } from "@/components/ui/sanity-image";
4
4
  import {
5
5
  getLinkAttributes,
6
6
  urlForReference,
7
- } from "../../lib/integrations/sanity/utils/link";
7
+ } from "@/lib/integrations/sanity/utils/link";
8
8
 
9
9
  interface RichTextProps {
10
10
  content: PortableTextProps["value"];
@@ -80,7 +80,9 @@ export const RichText = ({ content }: RichTextProps) => {
80
80
  externalLink: LinkMark,
81
81
  },
82
82
  block: {
83
- h1: ({ children }) => <h1 className="h1">{children}</h1>,
83
+ // The h1 style is retired: the page owns its H1, so legacy content
84
+ // drops a level.
85
+ h1: ({ children }) => <h2 className="h2">{children}</h2>,
84
86
  h2: ({ children }) => <h2 className="h2">{children}</h2>,
85
87
  h3: ({ children }) => <h3 className="h3">{children}</h3>,
86
88
  h4: ({ children }) => <h4 className="h4">{children}</h4>,
@@ -6,7 +6,7 @@ import { apiVersion } from "@/lib/integrations/sanity/env";
6
6
 
7
7
  const HIDDEN_KEYS_FIELD = "hiddenPageBuilderKeys";
8
8
 
9
- type ReferenceItem = { _key: string; _ref: string; _type: string };
9
+ type BlockItem = { _key: string; _type?: string };
10
10
 
11
11
  /**
12
12
  * Eye-icon toggle for a page builder placement's per-page visibility. Adds the
@@ -14,7 +14,7 @@ type ReferenceItem = { _key: string; _ref: string; _type: string };
14
14
  * filters those keys out at query time. Standalone control because Sanity's
15
15
  * built-in item menu isn't extensible via public props.
16
16
  */
17
- export function BlockActionsItem(props: ObjectItemProps<ReferenceItem>) {
17
+ export function BlockActionsItem(props: ObjectItemProps<BlockItem>) {
18
18
  const { value, renderDefault } = props;
19
19
  const client = useClient({ apiVersion });
20
20
  const toast = useToast();
@@ -0,0 +1,124 @@
1
+ import { Box, Button, Card, Code, Flex, Text } from "@sanity/ui";
2
+ import { useEffect, useRef, useState } from "react";
3
+
4
+ const VIEWPORTS = [
5
+ { id: "desktop", label: "Desktop", width: 1440 },
6
+ { id: "tablet", label: "Tablet", width: 834 },
7
+ { id: "mobile", label: "Mobile", width: 390 },
8
+ ] as const;
9
+
10
+ type ViewportId = (typeof VIEWPORTS)[number]["id"];
11
+
12
+ interface ComponentPreviewFrameProps {
13
+ notes?: string | undefined;
14
+ path: string;
15
+ }
16
+
17
+ export const ComponentPreviewFrame = ({
18
+ notes,
19
+ path,
20
+ }: ComponentPreviewFrameProps) => {
21
+ const stage = useRef<HTMLDivElement>(null);
22
+ const [box, setBox] = useState({ height: 0, width: 0 });
23
+ const [viewport, setViewport] = useState<ViewportId>("desktop");
24
+
25
+ useEffect(() => {
26
+ const element = stage.current;
27
+ if (!element) return;
28
+
29
+ const observer = new ResizeObserver(([entry]) => {
30
+ if (!entry) return;
31
+ const { height, width } = entry.contentRect;
32
+ setBox({ height, width });
33
+ });
34
+
35
+ observer.observe(element);
36
+ return () => observer.disconnect();
37
+ }, []);
38
+
39
+ const width = VIEWPORTS.find((item) => item.id === viewport)?.width ?? 1440;
40
+
41
+ const scale = box.width > 0 ? Math.min(1, box.width / width) : 1;
42
+ const height = box.height > 0 ? box.height / scale : 0;
43
+
44
+ return (
45
+ <Flex direction="column" height="fill">
46
+ <Card borderBottom padding={2} tone="transparent">
47
+ <Flex align="center" gap={2}>
48
+ <Flex gap={1}>
49
+ {VIEWPORTS.map((item) => (
50
+ <Button
51
+ fontSize={0}
52
+ key={item.id}
53
+ mode={item.id === viewport ? "default" : "bleed"}
54
+ onClick={() => setViewport(item.id)}
55
+ padding={2}
56
+ text={item.label}
57
+ tone={item.id === viewport ? "primary" : "default"}
58
+ />
59
+ ))}
60
+ </Flex>
61
+ <Box flex={1} />
62
+ <Code size={0}>
63
+ {width} × {Math.round(height)} · {Math.round(scale * 100)}%
64
+ </Code>
65
+ </Flex>
66
+ </Card>
67
+
68
+ {notes ? (
69
+ <Card borderBottom padding={3} tone="transparent">
70
+ <Text muted size={0}>
71
+ {notes}
72
+ </Text>
73
+ </Card>
74
+ ) : null}
75
+
76
+ <Box
77
+ flex={1}
78
+ style={{
79
+ background: "var(--card-muted-bg-color)",
80
+ overflow: "hidden",
81
+ position: "relative",
82
+ }}
83
+ >
84
+ <div
85
+ ref={stage}
86
+ style={{
87
+ display: "flex",
88
+ height: "100%",
89
+ justifyContent: "center",
90
+ width: "100%",
91
+ }}
92
+ >
93
+ {height > 0 ? (
94
+ <div
95
+ style={{
96
+ background: "var(--card-bg-color)",
97
+ border: "1px solid var(--card-border-color)",
98
+ borderRadius: 4,
99
+ boxShadow: "0 1px 12px rgb(0 0 0 / 0.12)",
100
+ flex: "none",
101
+ height: box.height,
102
+ overflow: "hidden",
103
+ width: width * scale,
104
+ }}
105
+ >
106
+ <iframe
107
+ src={path}
108
+ style={{
109
+ border: 0,
110
+ display: "block",
111
+ height,
112
+ transform: `scale(${scale})`,
113
+ transformOrigin: "top left",
114
+ width,
115
+ }}
116
+ title="Component preview"
117
+ />
118
+ </div>
119
+ ) : null}
120
+ </div>
121
+ </Box>
122
+ </Flex>
123
+ );
124
+ };
@@ -0,0 +1,25 @@
1
+ import { Card, Text } from "@sanity/ui";
2
+ import { ComponentPreviewFrame } from "./component-preview-frame";
3
+
4
+ interface ComponentPreviewPaneProps {
5
+ options?: Record<string, unknown>;
6
+ }
7
+
8
+ export const ComponentPreviewPane = ({
9
+ options,
10
+ }: ComponentPreviewPaneProps) => {
11
+ const path = options?.path as string | undefined;
12
+ const notes = options?.notes as string | undefined;
13
+
14
+ if (!path) {
15
+ return (
16
+ <Card padding={4}>
17
+ <Text muted size={1}>
18
+ This component has no preview route.
19
+ </Text>
20
+ </Card>
21
+ );
22
+ }
23
+
24
+ return <ComponentPreviewFrame notes={notes} path={path} />;
25
+ };
@@ -1,38 +1,18 @@
1
1
  import { type ArrayOfObjectsInputProps, useFormValue } from "sanity";
2
2
  import { PageBuilderInsertMenu } from "@/lib/integrations/sanity/components/page-builder-insert-menu";
3
- import { useStudioPreviewUrls } from "@/lib/integrations/sanity/components/use-studio-preview-urls";
4
3
  import {
5
- blockPreviewKey,
6
4
  getVisiblePageBuilderInsertMenuGroups,
7
5
  getVisiblePageBuilderMemberNames,
6
+ type PageBuilderDocumentType,
8
7
  type PageBuilderGroupName,
9
- pageBuilderReferenceMembers,
10
8
  } from "@/lib/integrations/sanity/page-builder-config";
11
9
 
12
- const BLOCK_PREVIEW_KEYS = pageBuilderReferenceMembers.map((member) =>
13
- blockPreviewKey(member.name),
14
- );
15
-
16
- function useInsertMenuViews() {
17
- const previewUrls = useStudioPreviewUrls(BLOCK_PREVIEW_KEYS);
18
-
19
- return [
20
- {
21
- name: "grid" as const,
22
- previewImageUrl: (schemaTypeName: string) =>
23
- previewUrls?.[blockPreviewKey(schemaTypeName)],
24
- },
25
- { name: "list" as const },
26
- ];
27
- }
28
-
29
10
  export function PageBuilderInput(props: ArrayOfObjectsInputProps) {
30
11
  const pageTypeValue = useFormValue(["type"]);
31
12
  const pageType =
32
13
  typeof pageTypeValue === "string"
33
14
  ? (pageTypeValue as PageBuilderGroupName)
34
15
  : undefined;
35
- const views = useInsertMenuViews();
36
16
  const visibleMemberNames = getVisiblePageBuilderMemberNames(pageType);
37
17
 
38
18
  return props.renderDefault({
@@ -40,17 +20,14 @@ export function PageBuilderInput(props: ArrayOfObjectsInputProps) {
40
20
  arrayFunctions: PageBuilderInsertMenu,
41
21
  schemaType: {
42
22
  ...props.schemaType,
43
- of: props.schemaType.of.filter(
44
- (member) =>
45
- typeof member.name === "string" &&
46
- visibleMemberNames.has(member.name),
23
+ of: props.schemaType.of.filter((member) =>
24
+ visibleMemberNames.has(member.name as PageBuilderDocumentType),
47
25
  ),
48
26
  options: {
49
27
  ...props.schemaType.options,
50
28
  insertMenu: {
51
29
  ...props.schemaType.options?.insertMenu,
52
30
  groups: getVisiblePageBuilderInsertMenuGroups(pageType),
53
- views,
54
31
  },
55
32
  },
56
33
  },