bsmnt 0.5.0 → 0.6.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 (123) hide show
  1. package/README.md +1 -0
  2. package/dist/application/add-hooks/index.d.ts.map +1 -1
  3. package/dist/application/add-hooks/index.js +1 -0
  4. package/dist/application/add-hooks/index.js.map +1 -1
  5. package/dist/application/add-integration/index.d.ts +13 -6
  6. package/dist/application/add-integration/index.d.ts.map +1 -1
  7. package/dist/application/add-integration/index.js +145 -62
  8. package/dist/application/add-integration/index.js.map +1 -1
  9. package/dist/application/create-project/build-context.d.ts +3 -1
  10. package/dist/application/create-project/build-context.d.ts.map +1 -1
  11. package/dist/application/create-project/build-context.js +2 -1
  12. package/dist/application/create-project/build-context.js.map +1 -1
  13. package/dist/application/create-project/index.d.ts +3 -1
  14. package/dist/application/create-project/index.d.ts.map +1 -1
  15. package/dist/application/create-project/index.js +4 -2
  16. package/dist/application/create-project/index.js.map +1 -1
  17. package/dist/application/install-skills/index.js +1 -1
  18. package/dist/application/install-skills/index.js.map +1 -1
  19. package/dist/core/create/execute-plan.d.ts +4 -0
  20. package/dist/core/create/execute-plan.d.ts.map +1 -1
  21. package/dist/core/create/execute-plan.js +8 -0
  22. package/dist/core/create/execute-plan.js.map +1 -1
  23. package/dist/core/create/types.d.ts +2 -0
  24. package/dist/core/create/types.d.ts.map +1 -1
  25. package/dist/domain/skills.d.ts +26 -0
  26. package/dist/domain/skills.d.ts.map +1 -1
  27. package/dist/domain/skills.js +13 -0
  28. package/dist/domain/skills.js.map +1 -1
  29. package/dist/index.js +53 -28
  30. package/dist/index.js.map +1 -1
  31. package/dist/infrastructure/create/executor.d.ts.map +1 -1
  32. package/dist/infrastructure/create/executor.js +5 -0
  33. package/dist/infrastructure/create/executor.js.map +1 -1
  34. package/dist/infrastructure/create/lifecycle-registry.d.ts.map +1 -1
  35. package/dist/infrastructure/create/lifecycle-registry.js +2 -0
  36. package/dist/infrastructure/create/lifecycle-registry.js.map +1 -1
  37. package/dist/infrastructure/create/setup-agent.d.ts +2 -0
  38. package/dist/infrastructure/create/setup-agent.d.ts.map +1 -1
  39. package/dist/infrastructure/create/setup-agent.js +9 -8
  40. package/dist/infrastructure/create/setup-agent.js.map +1 -1
  41. package/dist/infrastructure/create/setup-dotenvx.d.ts +41 -0
  42. package/dist/infrastructure/create/setup-dotenvx.d.ts.map +1 -0
  43. package/dist/infrastructure/create/setup-dotenvx.js +502 -0
  44. package/dist/infrastructure/create/setup-dotenvx.js.map +1 -0
  45. package/dist/infrastructure/create/setup-sanity.d.ts +13 -1
  46. package/dist/infrastructure/create/setup-sanity.d.ts.map +1 -1
  47. package/dist/infrastructure/create/setup-sanity.js +71 -31
  48. package/dist/infrastructure/create/setup-sanity.js.map +1 -1
  49. package/dist/modules/features/cms/sanity/config.d.ts.map +1 -1
  50. package/dist/modules/features/cms/sanity/config.js +2 -1
  51. package/dist/modules/features/cms/sanity/config.js.map +1 -1
  52. package/dist/modules/features/cms/sanity/mergers/layout.d.ts.map +1 -1
  53. package/dist/modules/features/cms/sanity/mergers/layout.js +10 -0
  54. package/dist/modules/features/cms/sanity/mergers/layout.js.map +1 -1
  55. package/dist/modules/features/env/dotenvx/config.d.ts +13 -0
  56. package/dist/modules/features/env/dotenvx/config.d.ts.map +1 -0
  57. package/dist/modules/features/env/dotenvx/config.js +47 -0
  58. package/dist/modules/features/env/dotenvx/config.js.map +1 -0
  59. package/dist/paths.d.ts +8 -0
  60. package/dist/paths.d.ts.map +1 -1
  61. package/dist/paths.js +8 -0
  62. package/dist/paths.js.map +1 -1
  63. package/package.json +5 -1
  64. package/src/agent-skills/json-ld/SKILL.md +85 -0
  65. package/src/agent-skills/manage-env/SKILL.md +67 -0
  66. package/src/modules/features/cms/sanity/files/app/blog/[slug]/page.tsx +48 -21
  67. package/src/modules/features/cms/sanity/files/components/sanity/structured-data.tsx +83 -0
  68. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/queries.ts +14 -0
  69. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/sanity.config.ts +19 -3
  70. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/sanity.types.ts +1 -0
  71. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/schemas/index.ts +36 -1
  72. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/schemas/metadata.ts +12 -1
  73. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/schemas/structured-data-blocks.ts +416 -0
  74. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/schemas/structured-data.ts +100 -0
  75. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/structure.ts +37 -2
  76. package/src/modules/features/cms/sanity/files/lib/integrations/sanity/utils/page-json-ld.ts +208 -0
  77. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/components/same-as-input.tsx +66 -0
  78. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/fetchers/layout.ts +20 -1
  79. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/icons.ts +17 -0
  80. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/queries.ts +22 -0
  81. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/index.ts +28 -0
  82. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/company-data.ts +8 -0
  83. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/layout/structured-data.ts +124 -0
  84. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/metadata.ts +12 -1
  85. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/schemas/shared/structured-data-blocks.ts +436 -0
  86. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/singletons.ts +13 -1
  87. package/src/modules/features/cms/sanity-pagebuilder/files/lib/integrations/sanity/utils/page-json-ld.ts +208 -0
  88. package/src/modules/features/cms/sanity-pagebuilder/files/lib/utils/json-ld.tsx +324 -15
  89. package/src/modules/features/env/dotenvx/files/decrypt-env.mjs +45 -0
  90. package/src/modules/features/env/dotenvx/files/gitleaks.yml +25 -0
  91. package/src/modules/features/env/dotenvx/files/instrumentation.ts +14 -0
  92. package/src/modules/features/env/dotenvx/files/push-keys.mjs +129 -0
  93. package/src/modules/features/env/dotenvx/files/save-key.mjs +129 -0
  94. package/src/modules/features/env/dotenvx/files/setup-env.mjs +67 -0
  95. package/src/modules/features/env/dotenvx/files/vercelignore +4 -0
  96. package/src/modules/features/env/dotenvx/files/write-env.mjs +47 -0
  97. package/src/templates/next-default/.env.example +14 -6
  98. package/src/templates/next-default/.github/workflows/ci.yml +22 -1
  99. package/src/templates/next-default/.vscode/extensions.json +3 -0
  100. package/src/templates/next-default/README.md +31 -0
  101. package/src/templates/next-default/lib/utils/json-ld.tsx +327 -21
  102. package/src/templates/next-default/next.config.ts +6 -0
  103. package/src/templates/next-experiments/.env.example +14 -6
  104. package/src/templates/next-experiments/.github/workflows/ci.yml +22 -1
  105. package/src/templates/next-experiments/.vscode/extensions.json +3 -0
  106. package/src/templates/next-experiments/README.md +31 -0
  107. package/src/templates/next-experiments/lib/utils/json-ld.tsx +327 -21
  108. package/src/templates/next-experiments/next.config.ts +6 -0
  109. package/src/templates/next-pagebuilder/.env.example +19 -1
  110. package/src/templates/next-pagebuilder/.github/workflows/ci.yml +21 -1
  111. package/src/templates/next-pagebuilder/.vscode/extensions.json +3 -0
  112. package/src/templates/next-pagebuilder/README.md +31 -0
  113. package/src/templates/next-pagebuilder/components/layout/json-ld/index.tsx +32 -19
  114. package/src/templates/next-pagebuilder/components/page-document/index.tsx +38 -0
  115. package/src/templates/next-pagebuilder/lib/integrations/sanity/sanity.types.ts +36 -2
  116. package/src/templates/next-pagebuilder/next.config.ts +6 -0
  117. package/src/templates/next-webgl/.env.example +14 -6
  118. package/src/templates/next-webgl/.github/workflows/ci.yml +22 -1
  119. package/src/templates/next-webgl/.vscode/extensions.json +3 -0
  120. package/src/templates/next-webgl/README.md +31 -0
  121. package/src/templates/next-webgl/lib/utils/json-ld.tsx +327 -21
  122. package/src/templates/next-webgl/next.config.ts +6 -0
  123. package/src/modules/features/cms/sanity/files/lib/utils/json-ld.tsx +0 -244
@@ -0,0 +1,67 @@
1
+ ---
2
+ name: manage-env
3
+ description: >-
4
+ Use when adding or changing an environment variable in a project that uses our
5
+ dotenvx setup. Decides secret vs trivial config: trivial/`NEXT_PUBLIC_*` the
6
+ agent handles fully, but for a secret the agent only wires the key in code
7
+ (process.env, validation schema, types) and hands the value-setting to the
8
+ human — it never types a secret value into a command. Triggers on "add an env
9
+ var", "new environment variable", "set an API key/token/secret", "put X in
10
+ .env", "encrypt env", or editing any `.env*` file.
11
+ ---
12
+
13
+ # Add or change an environment variable
14
+
15
+ This project uses **dotenvx**. Env files:
16
+
17
+ - `.env` — shared / development, committed **encrypted**
18
+ - `.env.preview`, `.env.production` — per-environment overrides, committed encrypted (created on demand)
19
+ - `.env.local` — plaintext, gitignored (personal local overrides)
20
+ - `.env.keys` — private keys, gitignored (backed up in the team password manager)
21
+
22
+ Scripts run `dotenvx run -f .env.$VERCEL_ENV -f .env -- …`, so the env-specific
23
+ file overrides `.env`. A **gitleaks** pre-commit hook + CI scan are the real
24
+ enforcement — this skill just makes the right choice the default.
25
+
26
+ ## 1. Secret or trivial config?
27
+
28
+ - **Secret** — API keys, tokens, DB URLs, anything you'd rotate if it leaked.
29
+ - **Trivial** — public base URLs, feature flags, anything `NEXT_PUBLIC_*` (it
30
+ ships to the browser anyway). Plaintext is fine; encrypting buys nothing.
31
+
32
+ ## 2a. Secret → never put the value in a command
33
+
34
+ A raw secret in a command (`env:set KEY <value>`) leaks the plaintext into the
35
+ tool call and the conversation context. So split the work:
36
+
37
+ - **Agent — code wiring only, never the value.** Read it via `process.env.KEY`,
38
+ add it to the env validation schema / types if the project has one, and add a
39
+ placeholder line to `.env.example`. Document the key name and where it's used.
40
+ - **Human — owns the value.** Either run `bun run env:set KEY <value>` yourself
41
+ (`--preview` / `--prod` for per-environment), or set it directly in the Vercel
42
+ environment. `env:set` encrypts in place and adds the matching
43
+ `DOTENV_PRIVATE_KEY*` to `.env.keys`.
44
+ - **Verify (agent):** only via `git diff` showing `KEY=encrypted:…`. Never run
45
+ `env:view` on a secret — it prints the decrypted value into context.
46
+ - Each `DOTENV_PRIVATE_KEY*` must exist on the matching Vercel environment and as a
47
+ CI secret, or it won't decrypt there.
48
+ - **Edge runtime caveat.** dotenvx only decrypts in the Node runtime (via
49
+ `instrumentation.ts`). A secret read in `middleware.ts` or a `runtime = "edge"`
50
+ route is **not** decrypted from `.env` — it must be set directly as a Vercel env
51
+ var. The encrypted `.env` covers Node functions + build, not the edge.
52
+
53
+ ## 2b. Trivial config → plaintext
54
+
55
+ Add it plaintext — no encryption — with `bun run env:set KEY value --plain`, or
56
+ just append a line to the file:
57
+
58
+ ```bash
59
+ NEXT_PUBLIC_FOO=bar
60
+ ```
61
+
62
+ gitleaks ignores non-secret plaintext, so it won't fail the scan.
63
+
64
+ ## Rule
65
+
66
+ Never commit a real secret in plaintext. If one lands there, bulk-encrypt it in
67
+ place with `bun run env:encrypt-all` (no value typed).
@@ -6,6 +6,9 @@ import {
6
6
  ALL_ARTICLES_QUERY,
7
7
  ARTICLE_QUERY,
8
8
  } from "@/lib/integrations/sanity/queries";
9
+ import { urlForImage } from "@/lib/integrations/sanity/utils/image";
10
+ import { buildPageJsonLd } from "@/lib/integrations/sanity/utils/page-json-ld";
11
+ import { generateArticleJsonLd, JsonLd } from "@/lib/utils/json-ld";
9
12
 
10
13
  export async function generateStaticParams() {
11
14
  if (!client) return [];
@@ -30,28 +33,52 @@ export default async function BlogArticlePage({ params }: Props) {
30
33
 
31
34
  if (!article) return notFound();
32
35
 
36
+ const articleUrl = `/blog/${slug}`;
37
+ const articleImage = article.featuredImage
38
+ ? urlForImage(article.featuredImage).url()
39
+ : article.metadata?.image
40
+ ? urlForImage(article.metadata.image).url()
41
+ : undefined;
42
+ const pageJsonLd = buildPageJsonLd(article.metadata?.jsonLd);
43
+
33
44
  return (
34
- <article className="container mx-auto px-4 py-8 max-w-4xl">
35
- <header className="mb-8">
36
- <h1 className="text-4xl font-bold mb-4">{article.title}</h1>
37
- {article.excerpt && (
38
- <p className="text-lg text-gray-600">{article.excerpt}</p>
39
- )}
40
- <div className="flex gap-4 mt-4 text-sm text-gray-500">
41
- {article.author && <span>By {article.author}</span>}
42
- {article.publishedAt && (
43
- <time dateTime={article.publishedAt}>
44
- {new Date(article.publishedAt).toLocaleDateString()}
45
- </time>
45
+ <>
46
+ <JsonLd
47
+ data={generateArticleJsonLd({
48
+ title: article.metadata?.title || article.title || "",
49
+ url: articleUrl,
50
+ description: article.metadata?.description || article.excerpt,
51
+ image: articleImage,
52
+ datePublished: article.publishedAt,
53
+ dateModified: article._updatedAt,
54
+ authorName: article.author,
55
+ })}
56
+ />
57
+ {pageJsonLd.map(({ key, data }) => (
58
+ <JsonLd key={key} data={data} />
59
+ ))}
60
+ <article className="container mx-auto px-4 py-8 max-w-4xl">
61
+ <header className="mb-8">
62
+ <h1 className="text-4xl font-bold mb-4">{article.title}</h1>
63
+ {article.excerpt && (
64
+ <p className="text-lg text-gray-600">{article.excerpt}</p>
46
65
  )}
47
- </div>
48
- </header>
49
-
50
- {article.content && (
51
- <div className="prose max-w-none">
52
- <RichText content={article.content} />
53
- </div>
54
- )}
55
- </article>
66
+ <div className="flex gap-4 mt-4 text-sm text-gray-500">
67
+ {article.author && <span>By {article.author}</span>}
68
+ {article.publishedAt && (
69
+ <time dateTime={article.publishedAt}>
70
+ {new Date(article.publishedAt).toLocaleDateString()}
71
+ </time>
72
+ )}
73
+ </div>
74
+ </header>
75
+
76
+ {article.content && (
77
+ <div className="prose max-w-none">
78
+ <RichText content={article.content} />
79
+ </div>
80
+ )}
81
+ </article>
82
+ </>
56
83
  );
57
84
  }
@@ -0,0 +1,83 @@
1
+ import { stegaClean } from "next-sanity";
2
+ import { sanityFetch } from "@/lib/integrations/sanity/live";
3
+ import { STRUCTURED_DATA_QUERY } from "@/lib/integrations/sanity/queries";
4
+ import {
5
+ generateOrganizationJsonLd,
6
+ generateWebSiteJsonLd,
7
+ JsonLd,
8
+ } from "@/lib/utils/json-ld";
9
+
10
+ type StructuredDataResult = {
11
+ name?: string | null;
12
+ legalName?: string | null;
13
+ description?: string | null;
14
+ url?: string | null;
15
+ logo?: string | null;
16
+ sameAs?: (string | null)[] | null;
17
+ contactPoint?: {
18
+ email?: string | null;
19
+ telephone?: string | null;
20
+ contactType?: string | null;
21
+ } | null;
22
+ address?: {
23
+ streetAddress?: string | null;
24
+ locality?: string | null;
25
+ region?: string | null;
26
+ postalCode?: string | null;
27
+ country?: string | null;
28
+ } | null;
29
+ } | null;
30
+
31
+ const nonEmpty = (value: unknown): string | undefined => {
32
+ const cleaned = stegaClean(value);
33
+ return typeof cleaned === "string" && cleaned.trim() !== ""
34
+ ? cleaned
35
+ : undefined;
36
+ };
37
+
38
+ /**
39
+ * Site-wide Organization + WebSite JSON-LD from the Structured Data
40
+ * singleton, with the static app values as fallback.
41
+ */
42
+ export async function SanityStructuredData({
43
+ fallbackName,
44
+ fallbackDescription,
45
+ fallbackLogo,
46
+ }: {
47
+ fallbackName: string;
48
+ fallbackDescription?: string;
49
+ fallbackLogo?: string;
50
+ }) {
51
+ const { data } = await sanityFetch({ query: STRUCTURED_DATA_QUERY });
52
+ // Deep-clean stega artifacts up front so nested objects (contactPoint,
53
+ // address) never leak visual-editing markers into the emitted JSON-LD.
54
+ const structuredData = stegaClean(data as unknown as StructuredDataResult);
55
+
56
+ const name = nonEmpty(structuredData?.name) ?? fallbackName;
57
+ const description =
58
+ nonEmpty(structuredData?.description) ?? fallbackDescription;
59
+ const url = nonEmpty(structuredData?.url);
60
+ const logo = nonEmpty(structuredData?.logo) ?? fallbackLogo;
61
+
62
+ const sameAs = (structuredData?.sameAs ?? [])
63
+ .map(nonEmpty)
64
+ .filter((value): value is string => Boolean(value));
65
+
66
+ return (
67
+ <>
68
+ <JsonLd data={generateWebSiteJsonLd({ name, url, description })} />
69
+ <JsonLd
70
+ data={generateOrganizationJsonLd({
71
+ name,
72
+ url,
73
+ logo,
74
+ description,
75
+ legalName: nonEmpty(structuredData?.legalName),
76
+ sameAs: sameAs.length ? sameAs : undefined,
77
+ contactPoint: structuredData?.contactPoint,
78
+ address: structuredData?.address,
79
+ })}
80
+ />
81
+ </>
82
+ );
83
+ }
@@ -64,3 +64,17 @@ export const ARTICLE_BY_ID_QUERY = defineQuery(`
64
64
  _updatedAt
65
65
  }
66
66
  `);
67
+
68
+ // Site-wide structured data (JSON-LD Organization + WebSite identity)
69
+ export const STRUCTURED_DATA_QUERY = defineQuery(`
70
+ *[_type == "structuredData"][0]{
71
+ name,
72
+ legalName,
73
+ description,
74
+ url,
75
+ "logo": logo.asset->url,
76
+ sameAs,
77
+ contactPoint,
78
+ address
79
+ }
80
+ `);
@@ -16,6 +16,10 @@ import { apiVersion, dataset, previewURL, projectId } from "./env";
16
16
  import { schema } from "./schemas";
17
17
  import { structure } from "./structure";
18
18
 
19
+ // Document types pinned to a single editable document.
20
+ const SINGLETON_TYPES = new Set(["structuredData"]);
21
+ const SINGLETON_ACTIONS = new Set(["publish", "discardChanges", "restore"]);
22
+
19
23
  // Helper function for URL resolution
20
24
  function resolveHref(documentType?: string, slug?: string): string | undefined {
21
25
  switch (documentType) {
@@ -77,15 +81,27 @@ export default defineConfig({
77
81
  media(),
78
82
  ],
79
83
  document: {
80
- actions: (prev) => {
84
+ actions: (prev, context) => {
81
85
  const isProd = process.env.NODE_ENV === "production";
82
- if (!isProd) return prev;
83
86
 
84
- return prev.map((action) =>
87
+ // Singletons can't be created or deleted from the document pane.
88
+ const filtered = SINGLETON_TYPES.has(context.schemaType)
89
+ ? prev.filter(
90
+ (action) =>
91
+ action.action !== undefined &&
92
+ SINGLETON_ACTIONS.has(action.action),
93
+ )
94
+ : prev;
95
+
96
+ if (!isProd) return filtered;
97
+
98
+ return filtered.map((action) =>
85
99
  action.action === "publish"
86
100
  ? createConfirmPublishAction(action)
87
101
  : action,
88
102
  );
89
103
  },
104
+ newDocumentOptions: (prev) =>
105
+ prev.filter((option) => !SINGLETON_TYPES.has(option.templateId)),
90
106
  },
91
107
  });
@@ -130,6 +130,7 @@ export type Metadata = {
130
130
  _type: "image";
131
131
  };
132
132
  noIndex?: boolean;
133
+ jsonLd?: Array<{ _key: string; _type: string; [key: string]: unknown }>;
133
134
  };
134
135
 
135
136
  export type RichText = Array<
@@ -13,9 +13,34 @@ import { link } from "./link";
13
13
  import { metadata } from "./metadata";
14
14
  import { navigation } from "./navigation";
15
15
  import { richText } from "./richText";
16
+ import { structuredData } from "./structured-data";
17
+ import {
18
+ customJsonLd,
19
+ eventJsonLd,
20
+ faqJsonLd,
21
+ itemListJsonLd,
22
+ jobPostingJsonLd,
23
+ softwareApplicationJsonLd,
24
+ videoJsonLd,
25
+ } from "./structured-data-blocks";
16
26
 
17
27
  // Re-export all schemas for convenience
18
- export { article, example, link, metadata, navigation, richText };
28
+ export {
29
+ article,
30
+ customJsonLd,
31
+ eventJsonLd,
32
+ example,
33
+ faqJsonLd,
34
+ itemListJsonLd,
35
+ jobPostingJsonLd,
36
+ link,
37
+ metadata,
38
+ navigation,
39
+ richText,
40
+ softwareApplicationJsonLd,
41
+ structuredData,
42
+ videoJsonLd,
43
+ };
19
44
 
20
45
  // Schema collection for Sanity configuration
21
46
  export const schema: { types: SchemaTypeDefinition[] } = {
@@ -25,11 +50,21 @@ export const schema: { types: SchemaTypeDefinition[] } = {
25
50
  metadata,
26
51
  richText,
27
52
 
53
+ // Page-level JSON-LD blocks (selectable in metadata)
54
+ faqJsonLd,
55
+ eventJsonLd,
56
+ videoJsonLd,
57
+ jobPostingJsonLd,
58
+ softwareApplicationJsonLd,
59
+ itemListJsonLd,
60
+ customJsonLd,
61
+
28
62
  // Document types (content pages)
29
63
  article,
30
64
  example,
31
65
 
32
66
  // Singleton types (one-off content)
33
67
  navigation,
68
+ structuredData,
34
69
  ],
35
70
  };
@@ -1,4 +1,5 @@
1
- import { defineField, defineType } from "sanity";
1
+ import { defineArrayMember, defineField, defineType } from "sanity";
2
+ import { pageJsonLdMembers } from "./structured-data-blocks";
2
3
 
3
4
  export const metadata = defineType({
4
5
  name: "metadata",
@@ -64,5 +65,15 @@ export const metadata = defineType({
64
65
  description: "Prevent search engines from indexing this page",
65
66
  initialValue: false,
66
67
  }),
68
+ defineField({
69
+ name: "jsonLd",
70
+ title: "Structured Data (JSON-LD)",
71
+ type: "array",
72
+ description:
73
+ "Page-level schema.org structured data emitted as JSON-LD. Pick a predefined type, or add a Custom JSON-LD block for anything not listed.",
74
+ of: pageJsonLdMembers.map((member) =>
75
+ defineArrayMember({ type: member.name }),
76
+ ),
77
+ }),
67
78
  ],
68
79
  });