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
@@ -10,27 +10,24 @@ import { resolve } from "node:path";
10
10
  const sanityDir = resolve(import.meta.dir, "../integrations/sanity");
11
11
 
12
12
  if (!process.env.NEXT_PUBLIC_SANITY_PROJECT_ID) {
13
+ // Annotate CI runs: the generated-files check verifies nothing without a project.
13
14
  console.info(
14
- " Skipping sanity:typegen — NEXT_PUBLIC_SANITY_PROJECT_ID not set",
15
+ process.env.CI
16
+ ? "::warning::Skipping sanity:typegen — NEXT_PUBLIC_SANITY_PROJECT_ID not set, so committed Sanity types aren't verified"
17
+ : " Skipping sanity:typegen — NEXT_PUBLIC_SANITY_PROJECT_ID not set",
15
18
  );
16
19
  process.exit(0);
17
20
  }
18
21
 
19
- // Format the generated file to the project's biome style so the committed types
20
- // match lint — but only locally. In CI/Vercel the build discards the formatted
21
- // result, so running biome there is pure per-deploy waste. (Scoped to the one
22
- // file; linting the whole dir would rewrite unrelated source.)
23
- const format =
24
- process.env.CI || process.env.VERCEL
25
- ? ""
26
- : " && bun biome check --write --unsafe sanity.types.ts";
22
+ // No formatting pass: `sanity.types.ts` is in biome's ignore list, so `biome check` on it exits 1.
27
23
 
28
24
  try {
29
25
  // No `--env-file` — dotenvx already injected the env into process.env.
30
26
  execSync(
31
- `cd ${sanityDir} && bun sanity schema extract && bun sanity typegen generate${format}`,
27
+ `cd ${sanityDir} && bun sanity schema extract && bun sanity typegen generate`,
32
28
  { stdio: "inherit" },
33
29
  );
34
30
  } catch {
35
- console.warn(" sanity:typegen failed — continuing anyway");
31
+ console.error(" sanity:typegen failed");
32
+ process.exit(1);
36
33
  }
@@ -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
+ };
@@ -7,9 +7,10 @@ import {
7
7
  import { hubspotFormField } from "./form-field"
8
8
 
9
9
  /**
10
- * Sample reusable HubSpot form block. Register it in your schema's document
11
- * list (and page-builder config, if you use one), then render its `form` with
12
- * <HubSpotForm /> from the integration core:
10
+ * Sample HubSpot form block. Register it in your schema's type list, then render
11
+ * its `form` with <HubSpotForm /> from the integration core. In a page-builder
12
+ * project re-declare it through `defineBlock` so it becomes an inline block —
13
+ * the `hubspot-pagebuilder-block` skill walks through it:
13
14
  *
14
15
  * @example
15
16
  * import { HubSpotForm } from "@/lib/integrations/hubspot/form"
@@ -51,10 +52,3 @@ export const hubspotFormBlock = defineType({
51
52
  },
52
53
  },
53
54
  })
54
-
55
- export const hubspotFormBlockReference = defineType({
56
- name: "hubspotFormBlockReference",
57
- title: "HubSpot Form Block",
58
- type: "reference",
59
- to: [{ type: "hubspotFormBlock" }],
60
- })
@@ -105,10 +105,15 @@ selectable block. Adding this integration installs the
105
105
  HubSpot form into the page builder" and it makes the edits and verifies the
106
106
  typecheck. To do it by hand:
107
107
 
108
- 1. `schemas/index.ts` — import `hubspotFormBlock`, add it to `schema.types`
108
+ 1. `schemas/components/reusable/hubspot-form.ts` — re-declare the adapter
109
+ through `defineBlock`, so the block is an inline object with Anchor ID and
110
+ Presentation Mode; export it from `schemas/components/index.ts` and add it to
111
+ `schema.types` in `schemas/index.ts`
109
112
  2. `page-builder-config.ts` — add `"hubspotFormBlock"` to
110
- `PageBuilderDocumentType` and a `hubspotFormBlockReference` member
111
- 3. `structure.ts` — add `S.documentTypeListItem("hubspotFormBlock")`
112
- 4. `components/page-builder/renderer.tsx` — add a `case "hubspotFormBlock"` that
113
- decodes `block.form` and renders `<HubSpotForm />`
113
+ `PageBuilderDocumentType` and a `pageBuilderBlockMembers` entry with
114
+ `pageTypes` and `category`
115
+ 3. `components/page-builder/registry.tsx` — one entry keyed `hubspotFormBlock`
116
+ that decodes `block.form` and renders `<HubSpotForm />`
117
+ 4. `components/page-builder/library.ts` — a fixture entry, which is what the
118
+ insert menu, Studio → Components and `/components` render
114
119
  5. `bun run sanity:typegen && bun typecheck`
@@ -44,10 +44,3 @@ export const ashbyJobBoardBlock = defineType({
44
44
  },
45
45
  },
46
46
  })
47
-
48
- export const ashbyJobBoardBlockReference = defineType({
49
- name: "ashbyJobBoardBlockReference",
50
- title: "Ashby Job Board",
51
- type: "reference",
52
- to: [{ type: "ashbyJobBoardBlock" }],
53
- })
@@ -125,10 +125,16 @@ selectable block. Adding this integration installs the
125
125
  Ashby job board into the page builder" and it makes the edits and verifies the
126
126
  typecheck. To do it by hand:
127
127
 
128
- 1. `schemas/index.ts` — import `ashbyJobBoardBlock`, add it to `schema.types`
128
+ 1. `schemas/components/reusable/ashby-job-board.ts` — re-declare the adapter
129
+ through `defineBlock`, so the block is an inline object with Anchor ID and
130
+ Presentation Mode; export it from `schemas/components/index.ts` and add it to
131
+ `schema.types` in `schemas/index.ts`
129
132
  2. `page-builder-config.ts` — add `"ashbyJobBoardBlock"` to
130
- `PageBuilderDocumentType` and an `ashbyJobBoardBlockReference` member
131
- 3. `structure.ts` — add `S.documentTypeListItem("ashbyJobBoardBlock")`
132
- 4. `components/page-builder/renderer.tsx` — add a `case "ashbyJobBoardBlock"`
133
- that awaits `getCareerJobs(block.boardName)` and renders the list
133
+ `PageBuilderDocumentType` and a `pageBuilderBlockMembers` entry with
134
+ `pageTypes` and `category`
135
+ 3. `components/page-builder/registry.tsx` — one entry keyed
136
+ `ashbyJobBoardBlock` that awaits `getCareerJobs(block.boardName)` and renders
137
+ the list
138
+ 4. `components/page-builder/library.ts` — a fixture entry, which is what the
139
+ insert menu, Studio → Components and `/components` render
134
140
  5. `bun run sanity:typegen && bun typecheck`
@@ -33,7 +33,9 @@
33
33
  # Development: http://localhost:3000
34
34
  # Production: https://your-domain.com
35
35
  # Inlined at build time — scope this to the Production environment only in
36
- # Vercel so Preview deployments fall through to VERCEL_PROJECT_PRODUCTION_URL.
36
+ # Vercel so Preview deployments fall through to their own VERCEL_URL. On a
37
+ # production deployment it is also the only host that gets indexed: every other
38
+ # host is sent `X-Robots-Tag: noindex` and a disallow-all robots.txt.
37
39
  NEXT_PUBLIC_BASE_URL=""
38
40
 
39
41
  # Draft mode secret for preview functionality
@@ -11,12 +11,13 @@ import {
11
11
  generateWebSiteJsonLd,
12
12
  JsonLd,
13
13
  } from "@/lib/utils/json-ld"
14
+ import { getBaseUrl } from "@/lib/utils/url"
14
15
 
15
16
  const APP_NAME = AppData.name
16
17
  const APP_DEFAULT_TITLE = "Basement Starter"
17
18
  const APP_TITLE_TEMPLATE = "%s - Basement Starter"
18
19
  const APP_DESCRIPTION = AppData.description
19
- const APP_BASE_URL = process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000"
20
+ const APP_BASE_URL = getBaseUrl()
20
21
 
21
22
  const geist = Geist({
22
23
  subsets: ["latin"],
@@ -1,12 +1,18 @@
1
1
  import type { MetadataRoute } from "next"
2
- import { getBaseUrl } from "@/lib/utils/url"
2
+ import { headers } from "next/headers"
3
+ import { getBaseUrl, isIndexableHost } from "@/lib/utils/url"
4
+
5
+ export default async function robots(): Promise<MetadataRoute.Robots> {
6
+ const host = (await headers()).get("host")
7
+ if (!isIndexableHost(host)) {
8
+ return { rules: { userAgent: "*", disallow: "/" } }
9
+ }
3
10
 
4
- export default function robots(): MetadataRoute.Robots {
5
11
  return {
6
12
  rules: {
7
13
  userAgent: "*",
8
14
  allow: "/",
9
- disallow: [],
15
+ disallow: ["/api/", "/studio"],
10
16
  },
11
17
  sitemap: `${getBaseUrl()}/sitemap.xml`,
12
18
  }
@@ -1,4 +1,5 @@
1
1
  import type { Metadata } from "next"
2
+ import { getBaseUrl } from "@/lib/utils/url"
2
3
 
3
4
  /**
4
5
  * Metadata Generation Utilities
@@ -26,7 +27,7 @@ interface GenerateMetadataOptions {
26
27
  authors?: string[]
27
28
  }
28
29
 
29
- const APP_BASE_URL = process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000"
30
+ const APP_BASE_URL = getBaseUrl()
30
31
 
31
32
  /**
32
33
  * Generate complete metadata object for pages
@@ -3,17 +3,34 @@
3
3
  *
4
4
  * Priority:
5
5
  * 1. NEXT_PUBLIC_BASE_URL - explicit override (custom domains)
6
- * 2. VERCEL_PROJECT_PRODUCTION_URL - auto-set by Vercel (production domain)
7
- * 3. VERCEL_URL - auto-set by Vercel (preview/branch deploys)
6
+ * 2. VERCEL_PROJECT_PRODUCTION_URL - auto-set by Vercel, on production deployments only
7
+ * 3. VERCEL_URL - the deployment's own URL, so previews never emit production URLs
8
8
  * 4. localhost fallback
9
9
  */
10
10
  export function getBaseUrl(): string {
11
11
  if (process.env.NEXT_PUBLIC_BASE_URL) {
12
12
  return process.env.NEXT_PUBLIC_BASE_URL
13
13
  }
14
- if (process.env.VERCEL_PROJECT_PRODUCTION_URL) {
14
+ if (
15
+ process.env.VERCEL_ENV === "production" &&
16
+ process.env.VERCEL_PROJECT_PRODUCTION_URL
17
+ ) {
15
18
  return `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`
16
19
  }
17
20
  if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`
18
21
  return "http://localhost:3000"
19
22
  }
23
+
24
+ // The production build also answers on *.vercel.app; only the launch domain is indexed.
25
+ export function getIndexableHost(): string | null {
26
+ const baseUrl = process.env.NEXT_PUBLIC_BASE_URL
27
+ // Off Vercel VERCEL_ENV is unset, and the configured host is the launch domain.
28
+ const vercelEnv = process.env.VERCEL_ENV
29
+ if (!baseUrl || (vercelEnv && vercelEnv !== "production")) return null
30
+ return URL.canParse(baseUrl) ? new URL(baseUrl).host : null
31
+ }
32
+
33
+ export function isIndexableHost(host: string | null | undefined): boolean {
34
+ const indexable = getIndexableHost()
35
+ return indexable !== null && host?.toLowerCase() === indexable
36
+ }
@@ -1,5 +1,9 @@
1
1
  import withBundleAnalyzer from "@next/bundle-analyzer"
2
2
  import type { NextConfig } from "next"
3
+ import { getIndexableHost } from "./lib/utils/url"
4
+
5
+ // Only the launch domain is indexed; every other host is sent noindex.
6
+ const indexableHost = getIndexableHost()
3
7
 
4
8
  const nextConfig: NextConfig = {
5
9
  reactStrictMode: true,
@@ -92,6 +96,18 @@ const nextConfig: NextConfig = {
92
96
  imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
93
97
  },
94
98
  headers: async () => [
99
+ {
100
+ source: "/:path*",
101
+ ...(indexableHost && {
102
+ missing: [
103
+ {
104
+ type: "host" as const,
105
+ value: indexableHost.replaceAll(".", "\\."),
106
+ },
107
+ ],
108
+ }),
109
+ headers: [{ key: "X-Robots-Tag", value: "noindex" }],
110
+ },
95
111
  {
96
112
  // Excludes /studio* — must stay mutually exclusive with the rule below.
97
113
  source: "/((?!studio(?:/|$)).*)",
@@ -33,7 +33,9 @@
33
33
  # Development: http://localhost:3000
34
34
  # Production: https://your-domain.com
35
35
  # Inlined at build time — scope this to the Production environment only in
36
- # Vercel so Preview deployments fall through to VERCEL_PROJECT_PRODUCTION_URL.
36
+ # Vercel so Preview deployments fall through to their own VERCEL_URL. On a
37
+ # production deployment it is also the only host that gets indexed: every other
38
+ # host is sent `X-Robots-Tag: noindex` and a disallow-all robots.txt.
37
39
  NEXT_PUBLIC_BASE_URL=""
38
40
 
39
41
  # Draft mode secret for preview functionality
@@ -11,12 +11,13 @@ import {
11
11
  generateWebSiteJsonLd,
12
12
  JsonLd,
13
13
  } from "@/lib/utils/json-ld"
14
+ import { getBaseUrl } from "@/lib/utils/url"
14
15
 
15
16
  const APP_NAME = AppData.name
16
17
  const APP_DEFAULT_TITLE = "Basement Starter"
17
18
  const APP_TITLE_TEMPLATE = "%s - Basement Starter"
18
19
  const APP_DESCRIPTION = AppData.description
19
- const APP_BASE_URL = process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000"
20
+ const APP_BASE_URL = getBaseUrl()
20
21
 
21
22
  const geist = Geist({
22
23
  subsets: ["latin"],
@@ -1,12 +1,18 @@
1
1
  import type { MetadataRoute } from "next"
2
- import { getBaseUrl } from "@/lib/utils/url"
2
+ import { headers } from "next/headers"
3
+ import { getBaseUrl, isIndexableHost } from "@/lib/utils/url"
4
+
5
+ export default async function robots(): Promise<MetadataRoute.Robots> {
6
+ const host = (await headers()).get("host")
7
+ if (!isIndexableHost(host)) {
8
+ return { rules: { userAgent: "*", disallow: "/" } }
9
+ }
3
10
 
4
- export default function robots(): MetadataRoute.Robots {
5
11
  return {
6
12
  rules: {
7
13
  userAgent: "*",
8
14
  allow: "/",
9
- disallow: [],
15
+ disallow: ["/api/", "/studio"],
10
16
  },
11
17
  sitemap: `${getBaseUrl()}/sitemap.xml`,
12
18
  }
@@ -1,4 +1,5 @@
1
1
  import type { Metadata } from "next"
2
+ import { getBaseUrl } from "@/lib/utils/url"
2
3
 
3
4
  /**
4
5
  * Metadata Generation Utilities
@@ -26,7 +27,7 @@ interface GenerateMetadataOptions {
26
27
  authors?: string[]
27
28
  }
28
29
 
29
- const APP_BASE_URL = process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3000"
30
+ const APP_BASE_URL = getBaseUrl()
30
31
 
31
32
  /**
32
33
  * Generate complete metadata object for pages