create-turbo-wizard 0.1.1 → 0.1.2

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 (3) hide show
  1. package/README.md +48 -15
  2. package/dist/cli.js +2708 -727
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -20,7 +20,7 @@ import { z } from "zod";
20
20
  function applyPreset(preset, overrides = {}) {
21
21
  return { ...PRESET_CONFIGS[preset], ...overrides };
22
22
  }
23
- var FRONTENDS, CONTENT_SOURCES, PACKAGE_MANAGERS, MODULES, FrontendSchema, ContentSourceSchema, PackageManagerSchema, ModuleSchema, ExperimentalSchema, ProjectNameSchema, WizardConfigSchema, wizardConfigJsonSchema = () => z.toJSONSchema(WizardConfigSchema), PRESETS, PRESET_CONFIGS;
23
+ var FRONTENDS, CONTENT_SOURCES, PACKAGE_MANAGERS, MODULES, DEPLOY_TARGETS, FrontendSchema, ContentSourceSchema, PackageManagerSchema, ModuleSchema, DeployTargetSchema, ExperimentalSchema, ProjectNameSchema, WizardConfigSchema, wizardConfigJsonSchema = () => z.toJSONSchema(WizardConfigSchema), PRESETS, PRESET_CONFIGS;
24
24
  var init_src = __esm(() => {
25
25
  FRONTENDS = ["astro", "next"];
26
26
  CONTENT_SOURCES = ["sanity", "markdown", "none"];
@@ -39,10 +39,12 @@ var init_src = __esm(() => {
39
39
  "forms",
40
40
  "llms"
41
41
  ];
42
+ DEPLOY_TARGETS = ["node", "vercel"];
42
43
  FrontendSchema = z.enum(FRONTENDS).describe("Web framework");
43
44
  ContentSourceSchema = z.enum(CONTENT_SOURCES).describe("Content source");
44
45
  PackageManagerSchema = z.enum(PACKAGE_MANAGERS).describe("Package manager");
45
46
  ModuleSchema = z.enum(MODULES).describe("Opt-in module");
47
+ DeployTargetSchema = z.enum(DEPLOY_TARGETS).describe("Deploy target (Astro SSR adapter; ignored by Next)");
46
48
  ExperimentalSchema = z.object({
47
49
  cacheComponents: z.boolean().default(false),
48
50
  reactCompiler: z.boolean().default(false)
@@ -53,6 +55,7 @@ var init_src = __esm(() => {
53
55
  frontend: FrontendSchema,
54
56
  content: ContentSourceSchema.default("none"),
55
57
  packageManager: PackageManagerSchema.default("pnpm"),
58
+ deploy: DeployTargetSchema.default("node"),
56
59
  modules: z.array(ModuleSchema).default([]),
57
60
  experimental: ExperimentalSchema
58
61
  });
@@ -72,7 +75,7 @@ var package_default;
72
75
  var init_package = __esm(() => {
73
76
  package_default = {
74
77
  name: "create-turbo-wizard",
75
- version: "0.1.1",
78
+ version: "0.1.2",
76
79
  description: "Scaffold a Turborepo monorepo: Astro or Next.js, Sanity or Markdown, opt-in modules.",
77
80
  keywords: [
78
81
  "turborepo",
@@ -289,6 +292,7 @@ ${WORKSPACE_GLOBS.map((g) => ` - "${g}"`).join(`
289
292
  "dist/",
290
293
  ".next/",
291
294
  ".astro/",
295
+ ".sanity/",
292
296
  ".turbo/",
293
297
  "*.tsbuildinfo",
294
298
  "",
@@ -380,8 +384,7 @@ var init_biome = __esm(() => {
380
384
  kind: "baseline",
381
385
  templates: [{ path: "biome.jsonc", content: BIOME_CONFIG }],
382
386
  scripts: { check: "biome check .", format: "biome format --write ." },
383
- deps: [{ name: "@biomejs/biome", versionKey: "@biomejs/biome", target: "root", dev: true }],
384
- owns: ["biome.jsonc"]
387
+ deps: [{ name: "@biomejs/biome", versionKey: "@biomejs/biome", target: "root", dev: true }]
385
388
  };
386
389
  });
387
390
 
@@ -426,8 +429,7 @@ var init_env = __esm(() => {
426
429
  content: j({ extends: "@workspace/typescript-config/base.json", include: ["src"] })
427
430
  },
428
431
  { path: "packages/env/src/index.ts", content: ENV_SRC }
429
- ],
430
- owns: ["packages/env/package.json", "packages/env/tsconfig.json", "packages/env/src/index.ts"]
432
+ ]
431
433
  };
432
434
  });
433
435
 
@@ -475,11 +477,6 @@ var init_logger = __esm(() => {
475
477
  content: j2({ extends: "@workspace/typescript-config/base.json", include: ["src"] })
476
478
  },
477
479
  { path: "packages/logger/src/index.ts", content: LOGGER_SRC }
478
- ],
479
- owns: [
480
- "packages/logger/package.json",
481
- "packages/logger/tsconfig.json",
482
- "packages/logger/src/index.ts"
483
480
  ]
484
481
  };
485
482
  });
@@ -543,12 +540,6 @@ var init_typescript_config = __esm(() => {
543
540
  compilerOptions: { lib: ["DOM", "DOM.Iterable", "ES2023"], jsx: "react-jsx" }
544
541
  })
545
542
  }
546
- ],
547
- owns: [
548
- "packages/typescript-config/package.json",
549
- "packages/typescript-config/base.json",
550
- "packages/typescript-config/nextjs.json",
551
- "packages/typescript-config/react-library.json"
552
543
  ]
553
544
  };
554
545
  });
@@ -616,7 +607,8 @@ function replaceNextMetadata(content, expr) {
616
607
  }
617
608
 
618
609
  // ../../packages/registry/src/content/markdown.ts
619
- var NEXT_MDX_COMPONENTS = `import type { MDXComponents } from "mdx/types";
610
+ var j4 = (value) => `${JSON.stringify(value, null, 2)}
611
+ `, CONTENT_PACKAGE_JSON, NEXT_MDX_COMPONENTS = `import type { MDXComponents } from "mdx/types";
620
612
 
621
613
  const components: MDXComponents = {
622
614
  // Override built-ins here, e.g. h1: ({ children }) => <h1 className="...">{children}</h1>,
@@ -642,15 +634,26 @@ This post is written in **MDX** — Markdown with JSX.
642
634
  - Mix Markdown prose with interactive components
643
635
  - Full JSX expression support
644
636
  - Server Components by default
637
+ `, NEXT_CONTENT_SCHEMA = `export interface PostMetadata {
638
+ title: string;
639
+ description?: string;
640
+ date?: string;
641
+ }
645
642
  `, NEXT_BLOG_POSTS_TS = `import { readdir } from "node:fs/promises";
646
643
  import path from "node:path";
644
+ import type { PostMetadata } from "@workspace/content/schema";
647
645
  import type { ComponentType } from "react";
648
646
 
649
- const POSTS_DIR = path.join(process.cwd(), "content/blog");
647
+ // cwd is apps/web for every next command turbo runs; the posts are two levels up in the
648
+ // workspace. In the default configuration this is read only at build time — the listing
649
+ // prerenders and generateStaticParams enumerates the slugs. With cacheComponents on, the
650
+ // PostList boundary below streams at request time and reads it then, exactly as it did
651
+ // when the posts lived in apps/web/content.
652
+ const POSTS_DIR = path.join(process.cwd(), "../../packages/content/blog");
650
653
 
651
654
  export interface PostModule {
652
655
  default: ComponentType;
653
- metadata: { title: string; description?: string; date?: string };
656
+ metadata: PostMetadata;
654
657
  }
655
658
 
656
659
  export async function getPostSlugs(): Promise<string[]> {
@@ -665,7 +668,7 @@ async function getPosts() {
665
668
  const slugs = await getPostSlugs();
666
669
  return Promise.all(
667
670
  slugs.map(async (slug) => {
668
- const { metadata } = (await import(\`@/content/blog/\${slug}.mdx\`)) as PostModule;
671
+ const { metadata } = (await import(\`@workspace/content/blog/\${slug}.mdx\`)) as PostModule;
669
672
  return { slug, ...metadata };
670
673
  }),
671
674
  );
@@ -719,7 +722,7 @@ export default async function BlogPostPage({
719
722
  params: Promise<{ slug: string }>;
720
723
  }) {
721
724
  const { slug } = await params;
722
- const post = (await import(\`@/content/blog/\${slug}.mdx\`).catch(() => null)) as PostModule | null;
725
+ const post = (await import(\`@workspace/content/blog/\${slug}.mdx\`).catch(() => null)) as PostModule | null;
723
726
  if (!post) notFound();
724
727
 
725
728
  const { default: Post, metadata } = post;
@@ -733,18 +736,23 @@ export default async function BlogPostPage({
733
736
  </main>
734
737
  );
735
738
  }
736
- `, ASTRO_CONTENT_CONFIG = `import { glob } from "astro/loaders";
737
- import { defineCollection, z } from "astro:content";
739
+ `, ASTRO_CONTENT_SCHEMA = `import { z } from "astro:content";
740
+
741
+ export const blogSchema = z.object({
742
+ title: z.string(),
743
+ description: z.string(),
744
+ pubDate: z.coerce.date(),
745
+ updatedDate: z.coerce.date().optional(),
746
+ });
747
+ `, ASTRO_CONTENT_CONFIG = `import { blogSchema } from "@workspace/content/schema";
748
+ import { glob } from "astro/loaders";
749
+ import { defineCollection } from "astro:content";
738
750
 
739
751
  const blog = defineCollection({
740
- // Pattern includes both .md and .mdx so the collection is mixed-format.
741
- loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/blog" }),
742
- schema: z.object({
743
- title: z.string(),
744
- description: z.string(),
745
- pubDate: z.coerce.date(),
746
- updatedDate: z.coerce.date().optional(),
747
- }),
752
+ // Pattern includes both .md and .mdx so the collection is mixed-format. \`base\` is
753
+ // resolved from the project root (apps/web), so it reaches out to the package.
754
+ loader: glob({ pattern: "**/*.{md,mdx}", base: "../../packages/content/blog" }),
755
+ schema: blogSchema,
748
756
  });
749
757
 
750
758
  export const collections = { blog };
@@ -822,6 +830,16 @@ const { Content } = await render(post);
822
830
  </html>
823
831
  `, markdown;
824
832
  var init_markdown = __esm(() => {
833
+ CONTENT_PACKAGE_JSON = j4({
834
+ name: "@workspace/content",
835
+ version: "0.0.0",
836
+ private: true,
837
+ type: "module",
838
+ exports: {
839
+ "./schema": "./src/schema.ts",
840
+ "./blog/*": "./blog/*"
841
+ }
842
+ });
825
843
  markdown = {
826
844
  id: "markdown",
827
845
  kind: "content",
@@ -829,21 +847,25 @@ var init_markdown = __esm(() => {
829
847
  label: "MDX content",
830
848
  hint: "Markdown + JSX wired into the frontend (official recipe)"
831
849
  },
850
+ templates: [{ path: "packages/content/package.json", content: CONTENT_PACKAGE_JSON }],
832
851
  fragments: {
833
852
  next: [
853
+ { path: "packages/content/src/schema.ts", content: NEXT_CONTENT_SCHEMA },
854
+ { path: "packages/content/blog/hello.mdx", content: NEXT_BLOG_POST_MDX },
834
855
  { path: "apps/web/mdx-components.tsx", content: NEXT_MDX_COMPONENTS },
835
- { path: "apps/web/content/blog/hello.mdx", content: NEXT_BLOG_POST_MDX },
836
856
  { path: "apps/web/app/blog/posts.ts", content: NEXT_BLOG_POSTS_TS },
837
857
  { path: "apps/web/app/blog/page.tsx", content: NEXT_BLOG_INDEX_TSX },
838
858
  { path: "apps/web/app/blog/[slug]/page.tsx", content: NEXT_BLOG_SLUG_TSX }
839
859
  ],
840
860
  astro: [
861
+ { path: "packages/content/src/schema.ts", content: ASTRO_CONTENT_SCHEMA },
862
+ { path: "packages/content/blog/hello.mdx", content: ASTRO_BLOG_POST },
841
863
  { path: "apps/web/src/content.config.ts", content: ASTRO_CONTENT_CONFIG },
842
- { path: "apps/web/src/content/blog/hello.mdx", content: ASTRO_BLOG_POST },
843
864
  { path: "apps/web/src/pages/blog/index.astro", content: ASTRO_BLOG_INDEX },
844
865
  { path: "apps/web/src/pages/blog/[...slug].astro", content: ASTRO_BLOG_SLUG_PAGE }
845
866
  ]
846
867
  },
868
+ deps: [{ name: "@workspace/content", version: "workspace:*", target: "app" }],
847
869
  frontendDeps: {
848
870
  next: [
849
871
  { name: "@next/mdx", version: "^16.2.0", target: "app", dev: true },
@@ -853,17 +875,6 @@ var init_markdown = __esm(() => {
853
875
  ],
854
876
  astro: [{ name: "@astrojs/mdx", version: "^4.3.14", target: "app" }]
855
877
  },
856
- owns: [
857
- "apps/web/mdx-components.tsx",
858
- "apps/web/content/blog/hello.mdx",
859
- "apps/web/app/blog/posts.ts",
860
- "apps/web/app/blog/page.tsx",
861
- "apps/web/app/blog/[slug]/page.tsx",
862
- "apps/web/src/content.config.ts",
863
- "apps/web/src/content/blog/hello.mdx",
864
- "apps/web/src/pages/blog/index.astro",
865
- "apps/web/src/pages/blog/[...slug].astro"
866
- ],
867
878
  patches: [
868
879
  {
869
880
  path: "apps/web/next.config.ts",
@@ -879,224 +890,398 @@ var init_markdown = __esm(() => {
879
890
  };
880
891
  });
881
892
 
882
- // ../../packages/registry/src/content/sanity.ts
883
- var SCHEMA_BLOCKS = `import { defineArrayMember, defineField, defineType } from "sanity";
884
-
885
- // Page-builder blocks are objects, not documents: they only exist inside a page.
886
-
887
- export const hero = defineType({
888
- name: "hero",
889
- title: "Hero",
890
- type: "object",
891
- fields: [
892
- defineField({ name: "heading", type: "string", validation: (rule) => rule.required() }),
893
- defineField({ name: "subheading", type: "text", rows: 2 }),
894
- defineField({
895
- name: "image",
896
- type: "image",
897
- options: { hotspot: true },
898
- fields: [defineField({ name: "alt", type: "string", title: "Alternative text" })],
899
- }),
900
- ],
901
- preview: {
902
- select: { title: "heading", media: "image" },
903
- prepare: ({ title, media }) => ({ title: title ?? "Hero", subtitle: "Hero", media }),
904
- },
905
- });
906
-
907
- export const richText = defineType({
908
- name: "richText",
909
- title: "Rich text",
910
- type: "object",
911
- fields: [
912
- defineField({ name: "heading", type: "string" }),
913
- defineField({ name: "content", type: "array", of: [defineArrayMember({ type: "block" })] }),
914
- ],
915
- preview: {
916
- select: { title: "heading" },
917
- prepare: ({ title }) => ({ title: title ?? "Rich text", subtitle: "Rich text" }),
918
- },
919
- });
893
+ // ../../packages/registry/src/content/sanity/astro.ts
894
+ var ASTRO_ENV = `export const projectId = import.meta.env.PUBLIC_SANITY_PROJECT_ID || "placeholder";
920
895
 
921
- export const cta = defineType({
922
- name: "cta",
923
- title: "Call to action",
924
- type: "object",
925
- fields: [
926
- defineField({ name: "heading", type: "string", validation: (rule) => rule.required() }),
927
- defineField({ name: "text", type: "text", rows: 2 }),
928
- defineField({ name: "buttonLabel", type: "string" }),
929
- defineField({ name: "buttonHref", type: "string" }),
930
- ],
931
- preview: {
932
- select: { title: "heading" },
933
- prepare: ({ title }) => ({ title: title ?? "Call to action", subtitle: "Call to action" }),
934
- },
935
- });
936
- `, SCHEMA_PAGE = `import { defineArrayMember, defineField, defineType } from "sanity";
896
+ export const dataset = import.meta.env.PUBLIC_SANITY_DATASET || "production";
937
897
 
938
- export const page = defineType({
939
- name: "page",
940
- title: "Page",
941
- type: "document",
942
- fields: [
943
- defineField({ name: "title", type: "string", validation: (rule) => rule.required() }),
944
- defineField({
945
- name: "slug",
946
- type: "slug",
947
- options: { source: "title" },
948
- validation: (rule) => rule.required(),
949
- }),
950
- defineField({ name: "description", type: "text", rows: 3 }),
951
- defineField({
952
- name: "pageBuilder",
953
- title: "Page builder",
954
- type: "array",
955
- of: [
956
- defineArrayMember({ type: "hero" }),
957
- defineArrayMember({ type: "richText" }),
958
- defineArrayMember({ type: "cta" }),
959
- ],
960
- }),
961
- ],
962
- // The home page is the \`page\` whose slug is "home" — see sanity/queries.ts.
963
- preview: { select: { title: "title", subtitle: "slug.current" } },
964
- });
965
- `, SCHEMA_SETTINGS = `import { defineField, defineType } from "sanity";
898
+ /** Server-only read token; required (and only read) when Visual Editing is enabled. */
899
+ export const readToken = import.meta.env.SANITY_API_READ_TOKEN ?? "";
966
900
 
967
- export const settings = defineType({
968
- name: "settings",
969
- title: "Settings",
970
- type: "document",
971
- fields: [
972
- defineField({ name: "siteTitle", type: "string", validation: (rule) => rule.required() }),
973
- defineField({ name: "siteDescription", type: "text", rows: 3 }),
974
- defineField({
975
- name: "socialImage",
976
- title: "Social share image",
977
- type: "image",
978
- options: { hotspot: true },
979
- }),
980
- ],
981
- preview: { select: { title: "siteTitle", media: "socialImage" } },
982
- });
983
- `, SCHEMA_INDEX = `import type { SchemaTypeDefinition } from "sanity";
984
- import { cta, hero, richText } from "./blocks";
985
- import { page } from "./page";
986
- import { settings } from "./settings";
901
+ /**
902
+ * The @sanity/astro Visual Editing switch. Off by default: overlays need a rendering
903
+ * server (\`astro dev\`, or an SSR adapter in production) plus SANITY_API_READ_TOKEN.
904
+ */
905
+ export const visualEditingEnabled =
906
+ import.meta.env.PUBLIC_SANITY_VISUAL_EDITING_ENABLED === "true";
987
907
 
988
- export const schemaTypes: SchemaTypeDefinition[] = [page, settings, hero, richText, cta];
989
- `, STRUCTURE = `import type { StructureResolver } from "sanity/structure";
908
+ /**
909
+ * False on a freshly scaffolded project. Guards every fetch so \`astro build\` never dials
910
+ * placeholder.api.sanity.io and dies; pages render an empty state instead.
911
+ */
912
+ export const hasSanityCredentials = projectId !== "placeholder";
913
+ `, ASTRO_FETCH = `import { sanityClient } from "sanity:client";
914
+ import { hasSanityCredentials, readToken, visualEditingEnabled } from "./env";
990
915
 
991
916
  /**
992
- * \`settings\` is a singleton: one document with a fixed id, edited in place instead of
993
- * listed. Everything else falls through to the default document-type lists.
917
+ * \`sanity:client\` is a Vite virtual module and this file lives in a linked workspace
918
+ * package, not in apps/web. Both halves of that were measured before the move: the
919
+ * integration's \`resolveId\` fires for any importer in the graph, and Vite still applies
920
+ * its \`import.meta.env\` substitution inside the package (a pnpm workspace link resolves
921
+ * to a real path outside node_modules, so it is treated as source, not as an external
922
+ * dependency). Without the second half every read would silently short-circuit on a
923
+ * "placeholder" project id and every route would still answer 200.
994
924
  *
995
- * The two sets feed sanity.config.ts's \`document.newDocumentOptions\` / \`document.actions\`
996
- * guards (the official singleton recipe), so a second settings document cannot be created.
925
+ * The single read path, following @sanity/astro's Visual Editing recipe: with
926
+ * PUBLIC_SANITY_VISUAL_EDITING_ENABLED=true it fetches the drafts perspective with
927
+ * stega source maps, which is what gives Presentation its click-to-edit overlays.
928
+ *
929
+ * Without credentials it returns null before touching the network, so \`astro build\`
930
+ * succeeds on a fresh scaffold. The catch covers a configured-but-unreachable dataset
931
+ * for the same reason: a starter should render empty, not crash.
997
932
  */
998
- export const singletonTypes = new Set(["settings"]);
999
-
1000
- /** Actions that keep a singleton editable without allowing create/delete/duplicate. */
1001
- export const singletonActions = new Set(["publish", "discardChanges", "restore"]);
1002
-
1003
- export const structure: StructureResolver = (S) =>
1004
- S.list()
1005
- .title("Content")
1006
- .items([
1007
- S.listItem()
1008
- .title("Settings")
1009
- .id("settings")
1010
- .child(S.document().schemaType("settings").documentId("settings")),
1011
- S.divider(),
1012
- S.documentTypeListItem("page").title("Pages"),
1013
- ]);
1014
- `, PRESENTATION = `import { defineDocuments, defineLocations } from "sanity/presentation";
1015
-
1016
- /** Route → document: lets Presentation open the right \`page\` for the URL in the iframe. */
1017
- export const mainDocuments = defineDocuments([
1018
- { route: "/", filter: \`_type == "page" && slug.current == "home"\` },
1019
- { route: "/:slug", filter: \`_type == "page" && slug.current == $slug\` },
1020
- ]);
1021
-
1022
- /** Document → routes: the "Used on …" links shown when editing a document. */
1023
- export const locations = {
1024
- settings: defineLocations({
1025
- locations: [{ title: "Home", href: "/" }],
1026
- message: "This document is used on all pages",
1027
- tone: "positive",
1028
- }),
1029
- page: defineLocations({
1030
- select: { title: "title", slug: "slug.current" },
1031
- resolve: (doc) => ({
1032
- locations: [
1033
- {
1034
- title: doc?.title || "Untitled",
1035
- href: doc?.slug === "home" ? "/" : \`/\${doc?.slug}\`,
1036
- },
1037
- ],
1038
- }),
1039
- }),
1040
- };
1041
- `, DOCUMENT_GUARD = ` document: {
1042
- newDocumentOptions: (prev, { creationContext }) =>
1043
- creationContext.type === "global"
1044
- ? prev.filter((template) => !singletonTypes.has(template.templateId))
1045
- : prev,
1046
- actions: (prev, { schemaType }) =>
1047
- singletonTypes.has(schemaType)
1048
- ? prev.filter(({ action }) => action && singletonActions.has(action))
1049
- : prev,
1050
- },`, QUERIES = `import { defineQuery } from "groq";
1051
-
1052
- const PAGE_FIELDS = /* groq */ \`
1053
- _id,
1054
- _type,
1055
- title,
1056
- description,
1057
- "slug": slug.current,
1058
- pageBuilder[]{ ... }
1059
- \`;
1060
-
1061
- export const HOME_PAGE_QUERY = defineQuery(
1062
- \`*[_type == "page" && slug.current == "home"][0]{\${PAGE_FIELDS}}\`,
1063
- );
933
+ export async function loadQuery<T>(
934
+ query: string,
935
+ params: Record<string, unknown> = {},
936
+ ): Promise<T | null> {
937
+ if (!hasSanityCredentials) return null;
938
+ if (visualEditingEnabled && !readToken) {
939
+ throw new Error(
940
+ "SANITY_API_READ_TOKEN is required when PUBLIC_SANITY_VISUAL_EDITING_ENABLED is true.",
941
+ );
942
+ }
943
+ try {
944
+ const { result } = await sanityClient.fetch<T>(query, params, {
945
+ filterResponse: false,
946
+ perspective: visualEditingEnabled ? "drafts" : "published",
947
+ resultSourceMap: visualEditingEnabled ? "withKeyArraySelector" : false,
948
+ stega: visualEditingEnabled,
949
+ ...(visualEditingEnabled ? { token: readToken } : {}),
950
+ useCdn: !visualEditingEnabled,
951
+ });
952
+ return result;
953
+ } catch (error) {
954
+ console.warn("[sanity] query failed:", error);
955
+ return null;
956
+ }
957
+ }
958
+ `, ASTRO_IMAGE = `import { createImageUrlBuilder } from "@sanity/image-url";
959
+ import { dataset, projectId } from "./env";
1064
960
 
1065
- export const PAGE_QUERY = defineQuery(
1066
- \`*[_type == "page" && slug.current == $slug][0]{\${PAGE_FIELDS}}\`,
1067
- );
961
+ const builder = createImageUrlBuilder({ projectId, dataset });
1068
962
 
1069
- export const PAGE_SLUGS_QUERY = defineQuery(
1070
- \`*[_type == "page" && defined(slug.current) && slug.current != "home"].slug.current\`,
1071
- );
963
+ /** Build a CDN URL for a Sanity image; null when the block has no asset. */
964
+ export function urlFor(source: unknown, width = 1200): string | null {
965
+ const ref = (source as { asset?: { _ref?: string } } | null)?.asset?._ref;
966
+ if (!ref) return null;
967
+ return builder.image(ref).width(width).auto("format").url();
968
+ }
969
+ `, ASTRO_PAGE_BUILDER = `---
970
+ import { urlFor } from "@workspace/sanity/image";
971
+ import type { PAGE_QUERY_RESULT } from "@workspace/sanity/types";
972
+ import { PortableText } from "astro-portabletext";
1072
973
 
1073
- export const SETTINGS_QUERY = defineQuery(
1074
- \`*[_type == "settings"][0]{ siteTitle, siteDescription, socialImage }\`,
1075
- );
1076
- `, SEED_NDJSON = `{"_id":"settings","_type":"settings","siteTitle":"Turbo Wizard","siteDescription":"A Turborepo monorepo scaffolded with create-turbo-wizard, with Sanity wired end to end."}
1077
- {"_id":"page-home","_type":"page","title":"Turbo Wizard","description":"The home page, served from Sanity.","slug":{"_type":"slug","current":"home"},"pageBuilder":[{"_key":"hero1","_type":"hero","heading":"Content from Sanity, rendered by your app","subheading":"This page lives in the Studio. Edit it there and watch it change here."},{"_key":"rich1","_type":"richText","heading":"How this fits together","content":[{"_key":"b1","_type":"block","style":"normal","markDefs":[],"children":[{"_key":"s1","_type":"span","marks":[],"text":"The schema lives in apps/web/sanity/schema. Queries live in apps/web/sanity/queries.ts and run through typegen, so results are typed. The page builder renders each block by its _type."}]}]},{"_key":"cta1","_type":"cta","heading":"Open the Studio","text":"Edit this page and reload.","buttonLabel":"Open Studio","buttonHref":"/studio"}]}
1078
- {"_id":"page-about","_type":"page","title":"About","description":"A second page, to show routing by slug.","slug":{"_type":"slug","current":"about"},"pageBuilder":[{"_key":"hero2","_type":"hero","heading":"About this starter","subheading":"Pages are routed by their slug."},{"_key":"rich2","_type":"richText","content":[{"_key":"b2","_type":"block","style":"normal","markDefs":[],"children":[{"_key":"s2","_type":"span","marks":[],"text":"Add a page in the Studio, give it a slug, and it appears at that path."}]}]}]}
1079
- `, NEXT_ENV = `export const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID || "placeholder";
974
+ // Derived from the generated query result rather than hand-written: the page builder's
975
+ // shape IS what the projection returns, so typegen is the only source that cannot drift.
976
+ type PageBlock = NonNullable<NonNullable<PAGE_QUERY_RESULT>["pageBuilder"]>[number];
977
+ type ButtonLink = NonNullable<Extract<PageBlock, { _type: "hero" }>["buttons"]>[number];
1080
978
 
1081
- export const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET || "production";
979
+ interface Props {
980
+ blocks: PageBlock[];
981
+ }
1082
982
 
1083
- // A recent pinned date, per Sanity's API-versioning guidance (the docs pin the date the
1084
- // project was set up; bump deliberately, never to "today").
1085
- export const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION || "2026-02-01";
983
+ const { blocks } = Astro.props;
1086
984
 
1087
- /** Server-only read token; empty when unset (draft mode and live previews need it). */
1088
- export const readToken = process.env.SANITY_API_READ_TOKEN ?? "";
985
+ const section = "margin: 3rem 0;";
986
+ const grid = "display: grid; gap: 1.5rem; grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));";
987
+ const card = "border: 1px solid #e5e5e5; border-radius: 0.75rem; padding: 1.25rem;";
1089
988
 
1090
- /** The embedded Studio's route — stega encodes it into every string it returns. */
1091
- export const studioUrl = "/studio";
989
+ const buttonProps = (button: ButtonLink) =>
990
+ button.openInNewTab ? { target: "_blank", rel: "noreferrer" } : {};
991
+ ---
1092
992
 
1093
- /**
1094
- * False on a freshly scaffolded project. Guards every fetch so the build never dials
1095
- * placeholder.api.sanity.io and dies; pages render an empty state instead.
1096
- */
1097
- export const hasSanityCredentials = projectId !== "placeholder";
1098
- `, NEXT_CLIENT = `import { createClient } from "next-sanity";
1099
- import { apiVersion, dataset, projectId, studioUrl } from "./env";
993
+ {
994
+ blocks.map((block) => {
995
+ if (block._type === "hero") {
996
+ const src = urlFor(block.image, 1600);
997
+ return (
998
+ <section style={section}>
999
+ {block.badge && <p style="opacity: 0.6; margin: 0;">{block.badge}</p>}
1000
+ <h2>{block.title}</h2>
1001
+ {block.richText && <PortableText value={block.richText} />}
1002
+ {!!block.buttons?.length && (
1003
+ <p style="display: flex; gap: 0.75rem; flex-wrap: wrap;">
1004
+ {block.buttons.map(
1005
+ (button) =>
1006
+ button.href && (
1007
+ <a href={button.href} {...buttonProps(button)}>
1008
+ {button.text ?? button.href}
1009
+ </a>
1010
+ ),
1011
+ )}
1012
+ </p>
1013
+ )}
1014
+ {src && <img src={src} alt={block.image?.alt ?? ""} />}
1015
+ </section>
1016
+ );
1017
+ }
1018
+ if (block._type === "richTextBlock") {
1019
+ return (
1020
+ <section style={section}>
1021
+ {block.eyebrow && <p style="opacity: 0.6; margin: 0;">{block.eyebrow}</p>}
1022
+ {block.title && <h2>{block.title}</h2>}
1023
+ {block.richText && <PortableText value={block.richText} />}
1024
+ </section>
1025
+ );
1026
+ }
1027
+ if (block._type === "featureCardsIcon") {
1028
+ return (
1029
+ <section style={section}>
1030
+ {block.eyebrow && <p style="opacity: 0.6; margin: 0;">{block.eyebrow}</p>}
1031
+ {block.title && <h2>{block.title}</h2>}
1032
+ {block.richText && <PortableText value={block.richText} />}
1033
+ <div style={grid}>
1034
+ {(block.cards ?? []).map((item) => (
1035
+ <div style={card}>
1036
+ {/* \`icon\` holds a Lucide icon name; render it with your icon set of choice. */}
1037
+ {item.icon && <p style="opacity: 0.6; margin: 0;">{item.icon}</p>}
1038
+ <h3>{item.title}</h3>
1039
+ {item.richText && <PortableText value={item.richText} />}
1040
+ </div>
1041
+ ))}
1042
+ </div>
1043
+ </section>
1044
+ );
1045
+ }
1046
+ if (block._type === "imageLinkCards") {
1047
+ return (
1048
+ <section style={section}>
1049
+ {block.eyebrow && <p style="opacity: 0.6; margin: 0;">{block.eyebrow}</p>}
1050
+ {block.title && <h2>{block.title}</h2>}
1051
+ {block.richText && <PortableText value={block.richText} />}
1052
+ <div style={grid}>
1053
+ {(block.cards ?? []).map((item) => {
1054
+ const src = urlFor(item.image, 800);
1055
+ return (
1056
+ <div style={card}>
1057
+ {src && <img src={src} alt={item.image?.alt ?? ""} />}
1058
+ <h3>
1059
+ {item.href ? (
1060
+ <a
1061
+ href={item.href}
1062
+ {...(item.openInNewTab ? { target: "_blank", rel: "noreferrer" } : {})}
1063
+ >
1064
+ {item.title}
1065
+ </a>
1066
+ ) : (
1067
+ item.title
1068
+ )}
1069
+ </h3>
1070
+ {item.description && <p>{item.description}</p>}
1071
+ </div>
1072
+ );
1073
+ })}
1074
+ </div>
1075
+ </section>
1076
+ );
1077
+ }
1078
+ if (block._type === "faqAccordion") {
1079
+ return (
1080
+ <section style={section}>
1081
+ {block.eyebrow && <p style="opacity: 0.6; margin: 0;">{block.eyebrow}</p>}
1082
+ {block.title && <h2>{block.title}</h2>}
1083
+ {block.subtitle && <p>{block.subtitle}</p>}
1084
+ {(block.faqs ?? []).map((faq) => (
1085
+ <details style="border-top: 1px solid #e5e5e5; padding: 0.75rem 0;">
1086
+ <summary>{faq.title}</summary>
1087
+ {faq.richText && <PortableText value={faq.richText} />}
1088
+ </details>
1089
+ ))}
1090
+ </section>
1091
+ );
1092
+ }
1093
+ if (block._type === "subscribeNewsletter") {
1094
+ // Presentational only: no action, no handler. Wire it to the \`forms\` module's
1095
+ // endpoint (or your own) when you want it to do something.
1096
+ return (
1097
+ <section style={section}>
1098
+ <h2>{block.title}</h2>
1099
+ {block.subTitle && <PortableText value={block.subTitle} />}
1100
+ <form style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
1101
+ <input type="email" name="email" placeholder="you@example.com" required />
1102
+ <button type="submit">Subscribe</button>
1103
+ </form>
1104
+ {block.helperText && <PortableText value={block.helperText} />}
1105
+ </section>
1106
+ );
1107
+ }
1108
+ if (block._type === "cta") {
1109
+ return (
1110
+ <section style={section}>
1111
+ {block.eyebrow && <p style="opacity: 0.6; margin: 0;">{block.eyebrow}</p>}
1112
+ <h2>{block.title}</h2>
1113
+ {block.richText && <PortableText value={block.richText} />}
1114
+ {!!block.buttons?.length && (
1115
+ <p style="display: flex; gap: 0.75rem; flex-wrap: wrap;">
1116
+ {block.buttons.map(
1117
+ (button) =>
1118
+ button.href && (
1119
+ <a href={button.href} {...buttonProps(button)}>
1120
+ {button.text ?? button.href}
1121
+ </a>
1122
+ ),
1123
+ )}
1124
+ </p>
1125
+ )}
1126
+ </section>
1127
+ );
1128
+ }
1129
+ return null;
1130
+ })
1131
+ }
1132
+ `, ASTRO_SLUG_PAGE = `---
1133
+ import { VisualEditing } from "@sanity/astro/visual-editing";
1134
+ import { visualEditingEnabled } from "@workspace/sanity/env";
1135
+ import { loadQuery } from "@workspace/sanity/fetch";
1136
+ import { PAGE_QUERY, PAGE_SLUGS_QUERY } from "@workspace/sanity/queries";
1137
+ import type { PAGE_QUERY_RESULT } from "@workspace/sanity/types";
1138
+ import PageBuilder from "@workspace/sanity-blocks/PageBuilder.astro";
1139
+
1140
+ export async function getStaticPaths() {
1141
+ // Returns [] without credentials, so a fresh scaffold builds with no CMS pages.
1142
+ const slugs = await loadQuery<(string | null)[]>(PAGE_SLUGS_QUERY);
1143
+ return (slugs ?? []).filter(Boolean).map((slug) => ({ params: { slug } }));
1144
+ }
1145
+
1146
+ const page = await loadQuery<PAGE_QUERY_RESULT>(PAGE_QUERY, { slug: Astro.params.slug });
1147
+ ---
1148
+
1149
+ <html lang="en">
1150
+ <head>
1151
+ <meta charset="utf-8" />
1152
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
1153
+ <title>{page?.title ?? "web"}</title>
1154
+ </head>
1155
+ <body style="font-family: system-ui, sans-serif; padding: 3rem;">
1156
+ <h1>{page?.title}</h1>
1157
+ <PageBuilder blocks={page?.pageBuilder ?? []} />
1158
+ <VisualEditing enabled={visualEditingEnabled} />
1159
+ </body>
1160
+ </html>
1161
+ `, ASTRO_BLOG_INDEX_PAGE = `---
1162
+ import { VisualEditing } from "@sanity/astro/visual-editing";
1163
+ import { visualEditingEnabled } from "@workspace/sanity/env";
1164
+ import { loadQuery } from "@workspace/sanity/fetch";
1165
+ import { urlFor } from "@workspace/sanity/image";
1166
+ import { BLOG_INDEX_QUERY } from "@workspace/sanity/queries";
1167
+ import type { BLOG_INDEX_QUERY_RESULT } from "@workspace/sanity/types";
1168
+
1169
+ const posts = (await loadQuery<BLOG_INDEX_QUERY_RESULT>(BLOG_INDEX_QUERY)) ?? [];
1170
+ ---
1171
+
1172
+ <html lang="en">
1173
+ <head>
1174
+ <meta charset="utf-8" />
1175
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
1176
+ <title>Blog</title>
1177
+ </head>
1178
+ <body style="font-family: system-ui, sans-serif; padding: 3rem;">
1179
+ <h1>Blog</h1>
1180
+ {
1181
+ posts.length === 0 && (
1182
+ <p>
1183
+ No posts yet — seed the dataset with <code>pnpm sanity:seed</code> from apps/studio,
1184
+ or write one in the Studio (<code>pnpm dev:studio</code>).
1185
+ </p>
1186
+ )
1187
+ }
1188
+ {
1189
+ posts.map((post) => {
1190
+ const src = urlFor(post.image, 800);
1191
+ const author = post.authors?.[0];
1192
+ return (
1193
+ <article style="border-top: 1px solid #e5e5e5; padding: 1.5rem 0;">
1194
+ {src && <img src={src} alt={post.image?.alt ?? ""} />}
1195
+ <h2>
1196
+ <a href={\`/blog/\${post.slug}\`}>{post.title}</a>
1197
+ </h2>
1198
+ <p style="opacity: 0.6;">
1199
+ {author?.name}
1200
+ {author?.name && post.publishedAt && " · "}
1201
+ {post.publishedAt && <time datetime={post.publishedAt}>{post.publishedAt}</time>}
1202
+ </p>
1203
+ {post.description && <p>{post.description}</p>}
1204
+ </article>
1205
+ );
1206
+ })
1207
+ }
1208
+ <VisualEditing enabled={visualEditingEnabled} />
1209
+ </body>
1210
+ </html>
1211
+ `, ASTRO_BLOG_POST_PAGE = `---
1212
+ import { VisualEditing } from "@sanity/astro/visual-editing";
1213
+ import { visualEditingEnabled } from "@workspace/sanity/env";
1214
+ import { loadQuery } from "@workspace/sanity/fetch";
1215
+ import { urlFor } from "@workspace/sanity/image";
1216
+ import { BLOG_QUERY, BLOG_SLUGS_QUERY } from "@workspace/sanity/queries";
1217
+ import type { BLOG_QUERY_RESULT } from "@workspace/sanity/types";
1218
+ import { PortableText } from "astro-portabletext";
1219
+
1220
+ export async function getStaticPaths() {
1221
+ // Returns [] without credentials, so a fresh scaffold builds with no posts.
1222
+ const slugs = await loadQuery<(string | null)[]>(BLOG_SLUGS_QUERY);
1223
+ return (slugs ?? []).filter(Boolean).map((slug) => ({ params: { slug } }));
1224
+ }
1225
+
1226
+ const post = await loadQuery<BLOG_QUERY_RESULT>(BLOG_QUERY, { slug: Astro.params.slug });
1227
+ const src = urlFor(post?.image, 1600);
1228
+ const author = post?.authors?.[0];
1229
+ ---
1230
+
1231
+ <html lang="en">
1232
+ <head>
1233
+ <meta charset="utf-8" />
1234
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
1235
+ <title>{post?.title ?? "Blog"}</title>
1236
+ {post?.description && <meta name="description" content={post.description} />}
1237
+ </head>
1238
+ <body style="font-family: system-ui, sans-serif; padding: 3rem;">
1239
+ <p><a href="/blog">← Blog</a></p>
1240
+ <h1>{post?.title}</h1>
1241
+ <p style="opacity: 0.6;">
1242
+ {author?.name && (author.position ? \`\${author.name}, \${author.position}\` : author.name)}
1243
+ {author?.name && post?.publishedAt && " · "}
1244
+ {post?.publishedAt && <time datetime={post.publishedAt}>{post.publishedAt}</time>}
1245
+ </p>
1246
+ {src && <img src={src} alt={post?.image?.alt ?? ""} />}
1247
+ {post?.richText && <PortableText value={post.richText} />}
1248
+ <VisualEditing enabled={visualEditingEnabled} />
1249
+ </body>
1250
+ </html>
1251
+ `, ASTRO_MODULE_REF = '/// <reference types="@sanity/astro/module" />', ASTRO_ENV_LOADER = `// Astro loads .env after this config runs, so pull it in ourselves for the Sanity ids.
1252
+ try {
1253
+ process.loadEnvFile(".env");
1254
+ } catch {
1255
+ // No .env — the placeholder ids below keep the build working.
1256
+ }
1257
+ `;
1258
+
1259
+ // ../../packages/registry/src/content/sanity/next.ts
1260
+ var NEXT_ENV = `export const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID || "placeholder";
1261
+
1262
+ export const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET || "production";
1263
+
1264
+ // A recent pinned date, per Sanity's API-versioning guidance (the docs pin the date the
1265
+ // project was set up; bump deliberately, never to "today").
1266
+ export const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION || "2026-02-01";
1267
+
1268
+ /** Server-only read token; empty when unset (draft mode and live previews need it). */
1269
+ export const readToken = process.env.SANITY_API_READ_TOKEN ?? "";
1270
+
1271
+ /**
1272
+ * Where the Studio lives — stega encodes it into every string it returns, so a
1273
+ * Visual Editing click-through lands on the right field. Absolute since the Studio
1274
+ * became its own app (\`sanity dev\` defaults to :3333).
1275
+ */
1276
+ export const studioUrl = process.env.NEXT_PUBLIC_SANITY_STUDIO_URL || "http://localhost:3333";
1277
+
1278
+ /**
1279
+ * False on a freshly scaffolded project. Guards every fetch so the build never dials
1280
+ * placeholder.api.sanity.io and dies; pages render an empty state instead.
1281
+ */
1282
+ export const hasSanityCredentials = projectId !== "placeholder";
1283
+ `, NEXT_CLIENT = `import { createClient } from "next-sanity";
1284
+ import { apiVersion, dataset, projectId, studioUrl } from "./env";
1100
1285
 
1101
1286
  export const client = createClient({
1102
1287
  projectId,
@@ -1109,17 +1294,33 @@ export const client = createClient({
1109
1294
  stega: { studioUrl },
1110
1295
  });
1111
1296
  `, NEXT_LIVE = `import { defineLive } from "next-sanity/live";
1112
- import { readToken } from "./env";
1297
+ import { hasSanityCredentials, readToken } from "./env";
1113
1298
  import { client } from "./client";
1114
1299
 
1115
1300
  // defineLive gives us live content plus draft-mode reads. The token is passed as
1116
1301
  // undefined rather than "" when unset — an empty string reads as "provided but invalid".
1117
- export const { sanityFetch, SanityLive } = defineLive({
1302
+ export const { sanityFetch, SanityLive: LiveComponent } = defineLive({
1118
1303
  client,
1119
1304
  serverToken: readToken || undefined,
1120
1305
  browserToken: readToken || undefined,
1121
1306
  });
1122
- `, NEXT_FETCH = `import type { QueryParams } from "next-sanity";
1307
+
1308
+ /**
1309
+ * On a fresh scaffold \`projectId\` is "placeholder", so the live socket dials
1310
+ * placeholder.api.sanity.io and next-sanity retries forever — the dev log fills with
1311
+ * "<SanityLive> is attempting to reconnect" and nothing else is readable. Render
1312
+ * nothing until there are credentials, the same guard \`loadQuery\` applies to reads.
1313
+ */
1314
+ export const SanityLive = hasSanityCredentials ? LiveComponent : () => null;
1315
+ `, NEXT_FETCH = `/// <reference path="./sanity.types.ts" />
1316
+ // ^ \`sanity typegen generate\` writes sanity.types.ts here (see apps/studio/sanity.cli.ts) and
1317
+ // its \`declare module "@sanity/client"\` block is what types every query result. Now that
1318
+ // the file lives outside apps/web it is no longer swept up by the app's tsconfig include,
1319
+ // so this reference is what pulls it into the program — without it every loadQuery result
1320
+ // silently degrades to \`unknown\` and the pages stop type-checking. A path reference cannot
1321
+ // be elided the way an unused import can. The file ships pre-generated; regenerate it with
1322
+ // \`extract\` + \`type\` from apps/studio.
1323
+ import type { QueryParams } from "next-sanity";
1123
1324
  import { client } from "./client";
1124
1325
  import { hasSanityCredentials } from "./env";
1125
1326
  import { sanityFetch } from "./live";
@@ -1173,52 +1374,234 @@ export function urlFor(source: unknown, width = 1200): string | null {
1173
1374
  return builder.image(ref).width(width).auto("format").url();
1174
1375
  }
1175
1376
  `, NEXT_PAGE_BUILDER = `import { PortableText, type PortableTextProps } from "@portabletext/react";
1377
+ import { urlFor } from "@workspace/sanity/image";
1176
1378
  import Image from "next/image";
1177
- import { urlFor } from "./image";
1178
1379
 
1179
1380
  type SanityImage = { asset?: { _ref?: string } | null; alt?: string | null } | null;
1381
+ type Rich = PortableTextProps["value"] | null;
1382
+
1383
+ /** queries.ts flattens every \`customUrl\` to an href, so nothing here branches on link type. */
1384
+ type ButtonLink = {
1385
+ _key: string;
1386
+ text?: string | null;
1387
+ variant?: string | null;
1388
+ href?: string | null;
1389
+ openInNewTab?: boolean | null;
1390
+ };
1180
1391
 
1181
1392
  export type PageBlock =
1182
- | { _key: string; _type: "hero"; heading?: string; subheading?: string; image?: SanityImage }
1183
- | { _key: string; _type: "richText"; heading?: string; content?: PortableTextProps["value"] }
1393
+ | {
1394
+ _key: string;
1395
+ _type: "hero";
1396
+ badge?: string | null;
1397
+ title?: string | null;
1398
+ richText?: Rich;
1399
+ image?: SanityImage;
1400
+ buttons?: ButtonLink[] | null;
1401
+ }
1402
+ | {
1403
+ _key: string;
1404
+ _type: "richTextBlock";
1405
+ eyebrow?: string | null;
1406
+ title?: string | null;
1407
+ richText?: Rich;
1408
+ }
1409
+ | {
1410
+ _key: string;
1411
+ _type: "featureCardsIcon";
1412
+ eyebrow?: string | null;
1413
+ title?: string | null;
1414
+ richText?: Rich;
1415
+ cards?:
1416
+ | { _key: string; icon?: string | null; title?: string | null; richText?: Rich }[]
1417
+ | null;
1418
+ }
1419
+ | {
1420
+ _key: string;
1421
+ _type: "imageLinkCards";
1422
+ eyebrow?: string | null;
1423
+ title?: string | null;
1424
+ richText?: Rich;
1425
+ buttons?: ButtonLink[] | null;
1426
+ cards?:
1427
+ | {
1428
+ _key: string;
1429
+ title?: string | null;
1430
+ description?: string | null;
1431
+ image?: SanityImage;
1432
+ href?: string | null;
1433
+ openInNewTab?: boolean | null;
1434
+ }[]
1435
+ | null;
1436
+ }
1437
+ | {
1438
+ _key: string;
1439
+ _type: "faqAccordion";
1440
+ eyebrow?: string | null;
1441
+ title?: string | null;
1442
+ subtitle?: string | null;
1443
+ faqs?: { _id: string; title?: string | null; richText?: Rich }[] | null;
1444
+ }
1445
+ | {
1446
+ _key: string;
1447
+ _type: "subscribeNewsletter";
1448
+ title?: string | null;
1449
+ subTitle?: Rich;
1450
+ helperText?: Rich;
1451
+ }
1184
1452
  | {
1185
1453
  _key: string;
1186
1454
  _type: "cta";
1187
- heading?: string;
1188
- text?: string;
1189
- buttonLabel?: string;
1190
- buttonHref?: string;
1455
+ eyebrow?: string | null;
1456
+ title?: string | null;
1457
+ richText?: Rich;
1458
+ buttons?: ButtonLink[] | null;
1191
1459
  };
1192
1460
 
1461
+ const section = { margin: "3rem 0" } as const;
1462
+ const grid = {
1463
+ display: "grid",
1464
+ gap: "1.5rem",
1465
+ gridTemplateColumns: "repeat(auto-fit, minmax(16rem, 1fr))",
1466
+ } as const;
1467
+ const card = { border: "1px solid #e5e5e5", borderRadius: "0.75rem", padding: "1.25rem" } as const;
1468
+
1469
+ function Eyebrow({ children }: { children?: string | null }) {
1470
+ return children ? <p style={{ opacity: 0.6, margin: 0 }}>{children}</p> : null;
1471
+ }
1472
+
1473
+ function Buttons({ items }: { items?: ButtonLink[] | null }) {
1474
+ if (!items?.length) return null;
1475
+ return (
1476
+ <p style={{ display: "flex", gap: "0.75rem", flexWrap: "wrap" }}>
1477
+ {items.map((button) =>
1478
+ button.href ? (
1479
+ <a
1480
+ key={button._key}
1481
+ href={button.href}
1482
+ {...(button.openInNewTab ? { target: "_blank", rel: "noreferrer" } : {})}
1483
+ >
1484
+ {button.text ?? button.href}
1485
+ </a>
1486
+ ) : null,
1487
+ )}
1488
+ </p>
1489
+ );
1490
+ }
1491
+
1193
1492
  function Block({ block }: { block: PageBlock }) {
1194
1493
  switch (block._type) {
1195
1494
  case "hero": {
1196
1495
  const src = urlFor(block.image, 1600);
1197
1496
  return (
1198
- <section>
1199
- <h2>{block.heading}</h2>
1200
- {block.subheading ? <p>{block.subheading}</p> : null}
1497
+ <section style={section}>
1498
+ <Eyebrow>{block.badge}</Eyebrow>
1499
+ <h2>{block.title}</h2>
1500
+ {block.richText ? <PortableText value={block.richText} /> : null}
1501
+ <Buttons items={block.buttons} />
1201
1502
  {src ? (
1202
1503
  <Image src={src} alt={block.image?.alt ?? ""} width={1600} height={900} />
1203
1504
  ) : null}
1204
1505
  </section>
1205
1506
  );
1206
1507
  }
1207
- case "richText":
1508
+ case "richTextBlock":
1509
+ return (
1510
+ <section style={section}>
1511
+ <Eyebrow>{block.eyebrow}</Eyebrow>
1512
+ {block.title ? <h2>{block.title}</h2> : null}
1513
+ {block.richText ? <PortableText value={block.richText} /> : null}
1514
+ </section>
1515
+ );
1516
+ case "featureCardsIcon":
1517
+ return (
1518
+ <section style={section}>
1519
+ <Eyebrow>{block.eyebrow}</Eyebrow>
1520
+ {block.title ? <h2>{block.title}</h2> : null}
1521
+ {block.richText ? <PortableText value={block.richText} /> : null}
1522
+ <div style={grid}>
1523
+ {(block.cards ?? []).map((item) => (
1524
+ <div key={item._key} style={card}>
1525
+ {/* \`icon\` holds a Lucide icon name; render it with your icon set of choice. */}
1526
+ {item.icon ? <p style={{ opacity: 0.6, margin: 0 }}>{item.icon}</p> : null}
1527
+ <h3>{item.title}</h3>
1528
+ {item.richText ? <PortableText value={item.richText} /> : null}
1529
+ </div>
1530
+ ))}
1531
+ </div>
1532
+ </section>
1533
+ );
1534
+ case "imageLinkCards":
1535
+ return (
1536
+ <section style={section}>
1537
+ <Eyebrow>{block.eyebrow}</Eyebrow>
1538
+ {block.title ? <h2>{block.title}</h2> : null}
1539
+ {block.richText ? <PortableText value={block.richText} /> : null}
1540
+ <div style={grid}>
1541
+ {(block.cards ?? []).map((item) => {
1542
+ const src = urlFor(item.image, 800);
1543
+ return (
1544
+ <div key={item._key} style={card}>
1545
+ {src ? (
1546
+ <Image src={src} alt={item.image?.alt ?? ""} width={800} height={450} />
1547
+ ) : null}
1548
+ <h3>
1549
+ {item.href ? (
1550
+ <a
1551
+ href={item.href}
1552
+ {...(item.openInNewTab ? { target: "_blank", rel: "noreferrer" } : {})}
1553
+ >
1554
+ {item.title}
1555
+ </a>
1556
+ ) : (
1557
+ item.title
1558
+ )}
1559
+ </h3>
1560
+ {item.description ? <p>{item.description}</p> : null}
1561
+ </div>
1562
+ );
1563
+ })}
1564
+ </div>
1565
+ <Buttons items={block.buttons} />
1566
+ </section>
1567
+ );
1568
+ case "faqAccordion":
1569
+ // <details> is the accordion — it needs no client component and behaves the same
1570
+ // in the Astro build, where this same markup is duplicated.
1571
+ return (
1572
+ <section style={section}>
1573
+ <Eyebrow>{block.eyebrow}</Eyebrow>
1574
+ {block.title ? <h2>{block.title}</h2> : null}
1575
+ {block.subtitle ? <p>{block.subtitle}</p> : null}
1576
+ {(block.faqs ?? []).map((faq) => (
1577
+ <details key={faq._id} style={{ borderTop: "1px solid #e5e5e5", padding: "0.75rem 0" }}>
1578
+ <summary>{faq.title}</summary>
1579
+ {faq.richText ? <PortableText value={faq.richText} /> : null}
1580
+ </details>
1581
+ ))}
1582
+ </section>
1583
+ );
1584
+ case "subscribeNewsletter":
1585
+ // Presentational only: no action, no handler. Wire it to the \`forms\` module's
1586
+ // endpoint (or your own) when you want it to do something.
1208
1587
  return (
1209
- <section>
1210
- {block.heading ? <h2>{block.heading}</h2> : null}
1211
- {block.content ? <PortableText value={block.content} /> : null}
1588
+ <section style={section}>
1589
+ <h2>{block.title}</h2>
1590
+ {block.subTitle ? <PortableText value={block.subTitle} /> : null}
1591
+ <form style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap" }}>
1592
+ <input type="email" name="email" placeholder="you@example.com" required />
1593
+ <button type="submit">Subscribe</button>
1594
+ </form>
1595
+ {block.helperText ? <PortableText value={block.helperText} /> : null}
1212
1596
  </section>
1213
1597
  );
1214
1598
  case "cta":
1215
1599
  return (
1216
- <section>
1217
- <h2>{block.heading}</h2>
1218
- {block.text ? <p>{block.text}</p> : null}
1219
- {block.buttonHref && block.buttonLabel ? (
1220
- <a href={block.buttonHref}>{block.buttonLabel}</a>
1221
- ) : null}
1600
+ <section style={section}>
1601
+ <Eyebrow>{block.eyebrow}</Eyebrow>
1602
+ <h2>{block.title}</h2>
1603
+ {block.richText ? <PortableText value={block.richText} /> : null}
1604
+ <Buttons items={block.buttons} />
1222
1605
  </section>
1223
1606
  );
1224
1607
  default:
@@ -1236,34 +1619,9 @@ export function PageBuilder({ blocks }: { blocks: PageBlock[] }) {
1236
1619
  </>
1237
1620
  );
1238
1621
  }
1239
- `, NEXT_SANITY_CONFIG, NEXT_SANITY_CLI = `import { defineCliConfig } from "sanity/cli";
1240
-
1241
- // Run from apps/web. \`schema extract\` + \`typegen generate\` need no credentials, so the
1242
- // build-time \`prebuild\` hook works on a fresh clone.
1243
- export default defineCliConfig({
1244
- api: {
1245
- projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID || "placeholder",
1246
- dataset: process.env.NEXT_PUBLIC_SANITY_DATASET || "production",
1247
- },
1248
- typegen: {
1249
- path: "./sanity/**/*.{ts,tsx}",
1250
- schema: "sanity/schema.json",
1251
- generates: "sanity/sanity.types.ts",
1252
- },
1253
- });
1254
- `, NEXT_STUDIO_PAGE = `import { NextStudio } from "next-sanity/studio";
1255
- import config from "../../../sanity.config";
1256
-
1257
- export const dynamic = "force-static";
1258
-
1259
- export { metadata, viewport } from "next-sanity/studio";
1260
-
1261
- export default function StudioPage() {
1262
- return <NextStudio config={config} />;
1263
- }
1264
- `, NEXT_DRAFT_MODE_ROUTE = `import { defineEnableDraftMode } from "next-sanity/draft-mode";
1265
- import { readToken } from "../../../../sanity/env";
1266
- import { client } from "../../../../sanity/client";
1622
+ `, NEXT_DRAFT_MODE_ROUTE = `import { client } from "@workspace/sanity/client";
1623
+ import { readToken } from "@workspace/sanity/env";
1624
+ import { defineEnableDraftMode } from "next-sanity/draft-mode";
1267
1625
 
1268
1626
  export const { GET } = defineEnableDraftMode({
1269
1627
  client: client.withConfig({ token: readToken }),
@@ -1301,10 +1659,10 @@ export function DisableDraftMode() {
1301
1659
  </a>
1302
1660
  );
1303
1661
  }
1304
- `, NEXT_HOME_PAGE = `import type { Metadata } from "next";
1305
- import { loadQuery } from "../sanity/fetch";
1306
- import { PageBuilder, type PageBlock } from "../sanity/page-builder";
1307
- import { HOME_PAGE_QUERY, SETTINGS_QUERY } from "../sanity/queries";
1662
+ `, NEXT_HOME_PAGE = `import { loadQuery } from "@workspace/sanity/fetch";
1663
+ import { HOME_PAGE_QUERY, SETTINGS_QUERY } from "@workspace/sanity/queries";
1664
+ import { PageBuilder, type PageBlock } from "@workspace/sanity-blocks/blocks";
1665
+ import type { Metadata } from "next";
1308
1666
 
1309
1667
  // stega: false — encoded metadata would leak invisible characters into <title>.
1310
1668
  export async function generateMetadata(): Promise<Metadata> {
@@ -1324,7 +1682,8 @@ export default async function HomePage() {
1324
1682
  <h1>No content yet</h1>
1325
1683
  <p>
1326
1684
  Set NEXT_PUBLIC_SANITY_PROJECT_ID in apps/web/.env.local, then seed the dataset:
1327
- <code> pnpm sanity:seed</code> from apps/web. Edit it at <a href="/studio">/studio</a>.
1685
+ <code> pnpm sanity:seed</code> from apps/studio. Edit it in the Studio:
1686
+ <code> pnpm dev:studio</code>.
1328
1687
  </p>
1329
1688
  </main>
1330
1689
  );
@@ -1337,10 +1696,10 @@ export default async function HomePage() {
1337
1696
  </main>
1338
1697
  );
1339
1698
  }
1340
- `, NEXT_SLUG_PAGE = `import { notFound } from "next/navigation";
1341
- import { loadQuery, loadStaticQuery } from "../../sanity/fetch";
1342
- import { PageBuilder, type PageBlock } from "../../sanity/page-builder";
1343
- import { PAGE_QUERY, PAGE_SLUGS_QUERY } from "../../sanity/queries";
1699
+ `, NEXT_SLUG_PAGE = `import { loadQuery, loadStaticQuery } from "@workspace/sanity/fetch";
1700
+ import { PAGE_QUERY, PAGE_SLUGS_QUERY } from "@workspace/sanity/queries";
1701
+ import { PageBuilder, type PageBlock } from "@workspace/sanity-blocks/blocks";
1702
+ import { notFound } from "next/navigation";
1344
1703
 
1345
1704
  export async function generateStaticParams() {
1346
1705
  const slugs = await loadStaticQuery(PAGE_SLUGS_QUERY);
@@ -1359,193 +1718,1877 @@ export default async function Page({ params }: { params: Promise<{ slug: string
1359
1718
  </main>
1360
1719
  );
1361
1720
  }
1362
- `, ASTRO_ENV = `export const projectId = import.meta.env.PUBLIC_SANITY_PROJECT_ID || "placeholder";
1721
+ `, NEXT_BLOG_INDEX_PAGE = `import { loadQuery } from "@workspace/sanity/fetch";
1722
+ import { urlFor } from "@workspace/sanity/image";
1723
+ import { BLOG_INDEX_QUERY } from "@workspace/sanity/queries";
1724
+ import type { Metadata } from "next";
1725
+ import Image from "next/image";
1363
1726
 
1364
- export const dataset = import.meta.env.PUBLIC_SANITY_DATASET || "production";
1727
+ export const metadata: Metadata = { title: "Blog" };
1365
1728
 
1366
- /** Server-only read token; required (and only read) when Visual Editing is enabled. */
1367
- export const readToken = import.meta.env.SANITY_API_READ_TOKEN ?? "";
1729
+ export default async function BlogIndexPage() {
1730
+ const posts = (await loadQuery(BLOG_INDEX_QUERY)) ?? [];
1368
1731
 
1369
- /**
1370
- * The @sanity/astro Visual Editing switch. Off by default: overlays need a rendering
1371
- * server (\`astro dev\`, or an SSR adapter in production) plus SANITY_API_READ_TOKEN.
1372
- */
1373
- export const visualEditingEnabled =
1374
- import.meta.env.PUBLIC_SANITY_VISUAL_EDITING_ENABLED === "true";
1732
+ return (
1733
+ <main style={{ fontFamily: "system-ui, sans-serif", padding: "3rem" }}>
1734
+ <h1>Blog</h1>
1735
+ {posts.length === 0 ? (
1736
+ <p>
1737
+ No posts yet — seed the dataset with <code>pnpm sanity:seed</code> from apps/studio,
1738
+ or write one in the Studio (<code>pnpm dev:studio</code>).
1739
+ </p>
1740
+ ) : null}
1741
+ {posts.map((post) => {
1742
+ const src = urlFor(post.image, 800);
1743
+ const author = post.authors?.[0];
1744
+ return (
1745
+ <article key={post._id} style={{ borderTop: "1px solid #e5e5e5", padding: "1.5rem 0" }}>
1746
+ {src ? <Image src={src} alt={post.image?.alt ?? ""} width={800} height={450} /> : null}
1747
+ <h2>
1748
+ <a href={\`/blog/\${post.slug}\`}>{post.title}</a>
1749
+ </h2>
1750
+ <p style={{ opacity: 0.6 }}>
1751
+ {author?.name ? <span>{author.name}</span> : null}
1752
+ {author?.name && post.publishedAt ? " · " : null}
1753
+ {post.publishedAt ? <time dateTime={post.publishedAt}>{post.publishedAt}</time> : null}
1754
+ </p>
1755
+ {post.description ? <p>{post.description}</p> : null}
1756
+ </article>
1757
+ );
1758
+ })}
1759
+ </main>
1760
+ );
1761
+ }
1762
+ `, NEXT_BLOG_POST_PAGE = `import { PortableText } from "@portabletext/react";
1763
+ import { loadQuery, loadStaticQuery } from "@workspace/sanity/fetch";
1764
+ import { urlFor } from "@workspace/sanity/image";
1765
+ import { BLOG_QUERY, BLOG_SLUGS_QUERY } from "@workspace/sanity/queries";
1766
+ import type { Metadata } from "next";
1767
+ import Image from "next/image";
1768
+ import { notFound } from "next/navigation";
1375
1769
 
1376
- /**
1377
- * False on a freshly scaffolded project. Guards every fetch so \`astro build\` never dials
1378
- * placeholder.api.sanity.io and dies; pages render an empty state instead.
1379
- */
1380
- export const hasSanityCredentials = projectId !== "placeholder";
1381
- `, ASTRO_FETCH = `import { sanityClient } from "sanity:client";
1382
- import { hasSanityCredentials, readToken, visualEditingEnabled } from "./env";
1770
+ export async function generateStaticParams() {
1771
+ const slugs = await loadStaticQuery(BLOG_SLUGS_QUERY);
1772
+ return (slugs ?? []).filter((slug): slug is string => Boolean(slug)).map((slug) => ({ slug }));
1773
+ }
1774
+
1775
+ // stega: false — encoded metadata would leak invisible characters into <title>.
1776
+ export async function generateMetadata({
1777
+ params,
1778
+ }: {
1779
+ params: Promise<{ slug: string }>;
1780
+ }): Promise<Metadata> {
1781
+ const { slug } = await params;
1782
+ const post = await loadQuery(BLOG_QUERY, { slug }, { stega: false });
1783
+ return { title: post?.title ?? "Blog", description: post?.description ?? undefined };
1784
+ }
1785
+
1786
+ export default async function BlogPostPage({ params }: { params: Promise<{ slug: string }> }) {
1787
+ const { slug } = await params;
1788
+ const post = await loadQuery(BLOG_QUERY, { slug });
1789
+ if (!post) notFound();
1790
+
1791
+ const src = urlFor(post.image, 1600);
1792
+ const author = post.authors?.[0];
1793
+
1794
+ return (
1795
+ <main style={{ fontFamily: "system-ui, sans-serif", padding: "3rem" }}>
1796
+ <p>
1797
+ <a href="/blog">← Blog</a>
1798
+ </p>
1799
+ <h1>{post.title}</h1>
1800
+ <p style={{ opacity: 0.6 }}>
1801
+ {author?.name ? <span>{author.position ? \`\${author.name}, \${author.position}\` : author.name}</span> : null}
1802
+ {author?.name && post.publishedAt ? " · " : null}
1803
+ {post.publishedAt ? <time dateTime={post.publishedAt}>{post.publishedAt}</time> : null}
1804
+ </p>
1805
+ {src ? <Image src={src} alt={post.image?.alt ?? ""} width={1600} height={900} /> : null}
1806
+ {post.richText ? <PortableText value={post.richText} /> : null}
1807
+ </main>
1808
+ );
1809
+ }
1810
+ `;
1811
+
1812
+ // ../../packages/registry/src/content/sanity/schema.ts
1813
+ var SCHEMA_DEFINITIONS = `import { defineArrayMember, defineField, defineType } from "sanity";
1383
1814
 
1384
1815
  /**
1385
- * The single read path, following @sanity/astro's Visual Editing recipe: with
1386
- * PUBLIC_SANITY_VISUAL_EDITING_ENABLED=true it fetches the drafts perspective with
1387
- * stega source maps, which is what gives Presentation its click-to-edit overlays.
1816
+ * A link that resolves to either an internal document or an external URL. Blocks store the
1817
+ * choice; queries.ts turns it into a plain \`href\` string so components never branch on it.
1818
+ */
1819
+ export const customUrl = defineType({
1820
+ name: "customUrl",
1821
+ title: "URL",
1822
+ type: "object",
1823
+ fields: [
1824
+ defineField({
1825
+ name: "type",
1826
+ type: "string",
1827
+ initialValue: () => "external",
1828
+ options: { list: ["internal", "external"], layout: "radio", direction: "horizontal" },
1829
+ validation: (rule) => rule.required(),
1830
+ }),
1831
+ defineField({
1832
+ name: "openInNewTab",
1833
+ title: "Open in new tab",
1834
+ type: "boolean",
1835
+ initialValue: () => false,
1836
+ }),
1837
+ defineField({
1838
+ name: "external",
1839
+ title: "URL",
1840
+ type: "string",
1841
+ description: "A full address (https://…) or a path (/about).",
1842
+ hidden: ({ parent }) => parent?.type !== "external",
1843
+ validation: (rule) =>
1844
+ rule.custom((value, { parent }) =>
1845
+ (parent as { type?: string })?.type === "external" && !value
1846
+ ? "URL can't be empty"
1847
+ : true,
1848
+ ),
1849
+ }),
1850
+ defineField({
1851
+ name: "internal",
1852
+ type: "reference",
1853
+ to: [{ type: "page" }, { type: "blog" }],
1854
+ options: { disableNew: true },
1855
+ hidden: ({ parent }) => parent?.type !== "internal",
1856
+ validation: (rule) =>
1857
+ rule.custom((value, { parent }) =>
1858
+ (parent as { type?: string })?.type === "internal" && !value?._ref
1859
+ ? "Pick a document to link to"
1860
+ : true,
1861
+ ),
1862
+ }),
1863
+ ],
1864
+ preview: {
1865
+ select: { type: "type", external: "external", internal: "internal.slug.current" },
1866
+ prepare: ({ type, external, internal }) => ({
1867
+ title: type === "external" ? "External link" : "Internal link",
1868
+ subtitle: type === "external" ? external : internal,
1869
+ }),
1870
+ },
1871
+ });
1872
+
1873
+ export const button = defineType({
1874
+ name: "button",
1875
+ title: "Button",
1876
+ type: "object",
1877
+ fields: [
1878
+ defineField({
1879
+ name: "variant",
1880
+ type: "string",
1881
+ initialValue: () => "default",
1882
+ options: {
1883
+ list: ["default", "secondary", "outline", "link"],
1884
+ layout: "radio",
1885
+ direction: "horizontal",
1886
+ },
1887
+ }),
1888
+ defineField({ name: "text", title: "Button text", type: "string" }),
1889
+ defineField({ name: "url", title: "URL", type: "customUrl" }),
1890
+ ],
1891
+ preview: {
1892
+ select: { title: "text", variant: "variant" },
1893
+ prepare: ({ title, variant }) => ({
1894
+ title: title ?? "Untitled button",
1895
+ subtitle: variant ?? "default",
1896
+ }),
1897
+ },
1898
+ });
1899
+
1900
+ /**
1901
+ * Portable Text. Registered as a named type so documents and blocks can say
1902
+ * \`type: "richText"\` — the same indirection the reference uses.
1903
+ */
1904
+ export const richText = defineType({
1905
+ name: "richText",
1906
+ type: "array",
1907
+ of: [
1908
+ defineArrayMember({
1909
+ name: "block",
1910
+ type: "block",
1911
+ styles: [
1912
+ { title: "Normal", value: "normal" },
1913
+ { title: "H2", value: "h2" },
1914
+ { title: "H3", value: "h3" },
1915
+ { title: "H4", value: "h4" },
1916
+ ],
1917
+ lists: [
1918
+ { title: "Bullet", value: "bullet" },
1919
+ { title: "Numbered", value: "number" },
1920
+ ],
1921
+ marks: {
1922
+ decorators: [
1923
+ { title: "Strong", value: "strong" },
1924
+ { title: "Emphasis", value: "em" },
1925
+ { title: "Code", value: "code" },
1926
+ ],
1927
+ annotations: [
1928
+ {
1929
+ name: "customLink",
1930
+ title: "Link",
1931
+ type: "object",
1932
+ fields: [defineField({ name: "customLink", type: "customUrl" })],
1933
+ },
1934
+ ],
1935
+ },
1936
+ }),
1937
+ defineArrayMember({
1938
+ name: "image",
1939
+ title: "Image",
1940
+ type: "image",
1941
+ options: { hotspot: true },
1942
+ fields: [defineField({ name: "caption", title: "Caption", type: "string" })],
1943
+ }),
1944
+ ],
1945
+ });
1946
+ `, SCHEMA_BLOCKS = `import { defineArrayMember, defineField, defineType } from "sanity";
1947
+
1948
+ // Page-builder blocks are objects, not documents: they only exist inside a page.
1949
+ // Field names match turbo-start-sanity's, so its components drop in unchanged.
1950
+
1951
+ const imageField = defineField({
1952
+ name: "image",
1953
+ type: "image",
1954
+ options: { hotspot: true },
1955
+ fields: [
1956
+ defineField({
1957
+ name: "alt",
1958
+ title: "Alt text",
1959
+ type: "string",
1960
+ validation: (rule) =>
1961
+ rule.custom((value, context) =>
1962
+ (context.parent as { asset?: unknown })?.asset && !value?.trim()
1963
+ ? "Alt text is required when an image is set"
1964
+ : true,
1965
+ ),
1966
+ }),
1967
+ ],
1968
+ });
1969
+
1970
+ const buttonsField = defineField({
1971
+ name: "buttons",
1972
+ type: "array",
1973
+ of: [defineArrayMember({ type: "button" })],
1974
+ });
1975
+
1976
+ export const hero = defineType({
1977
+ name: "hero",
1978
+ title: "Hero",
1979
+ type: "object",
1980
+ fields: [
1981
+ defineField({ name: "badge", type: "string" }),
1982
+ defineField({ name: "title", type: "string" }),
1983
+ defineField({ name: "richText", type: "richText" }),
1984
+ imageField,
1985
+ buttonsField,
1986
+ ],
1987
+ preview: {
1988
+ select: { title: "title", media: "image" },
1989
+ prepare: ({ title, media }) => ({ title: title ?? "Hero", subtitle: "Hero", media }),
1990
+ },
1991
+ });
1992
+
1993
+ export const cta = defineType({
1994
+ name: "cta",
1995
+ title: "Call to action",
1996
+ type: "object",
1997
+ fields: [
1998
+ defineField({ name: "eyebrow", type: "string" }),
1999
+ defineField({ name: "title", type: "string" }),
2000
+ defineField({ name: "richText", type: "richText" }),
2001
+ buttonsField,
2002
+ ],
2003
+ preview: {
2004
+ select: { title: "title" },
2005
+ prepare: ({ title }) => ({ title: title ?? "Call to action", subtitle: "CTA" }),
2006
+ },
2007
+ });
2008
+
2009
+ export const richTextBlock = defineType({
2010
+ name: "richTextBlock",
2011
+ title: "Rich text",
2012
+ type: "object",
2013
+ fields: [
2014
+ defineField({ name: "eyebrow", type: "string" }),
2015
+ defineField({ name: "title", type: "string" }),
2016
+ defineField({ name: "richText", type: "richText" }),
2017
+ ],
2018
+ preview: {
2019
+ select: { title: "title" },
2020
+ prepare: ({ title }) => ({ title: title ?? "Rich text", subtitle: "Rich text" }),
2021
+ },
2022
+ });
2023
+
2024
+ // The reference stores \`icon\` as a \`lucide-icon\`, a custom input from
2025
+ // sanity-plugin-lucide-icon-picker. A plain string keeps the field (and the GROQ) the same
2026
+ // shape without a Studio plugin — add the plugin later and only this field changes.
2027
+ const featureCardIcon = defineArrayMember({
2028
+ name: "featureCardIcon",
2029
+ type: "object",
2030
+ fields: [
2031
+ defineField({
2032
+ name: "icon",
2033
+ type: "string",
2034
+ description: 'A Lucide icon name, e.g. "zap" or "shield-check".',
2035
+ }),
2036
+ defineField({ name: "title", type: "string" }),
2037
+ defineField({ name: "richText", type: "richText" }),
2038
+ ],
2039
+ preview: { select: { title: "title", subtitle: "icon" } },
2040
+ });
2041
+
2042
+ export const featureCardsIcon = defineType({
2043
+ name: "featureCardsIcon",
2044
+ title: "Feature cards",
2045
+ type: "object",
2046
+ fields: [
2047
+ defineField({ name: "eyebrow", type: "string" }),
2048
+ defineField({ name: "title", type: "string" }),
2049
+ defineField({ name: "richText", type: "richText" }),
2050
+ defineField({ name: "cards", type: "array", of: [featureCardIcon] }),
2051
+ ],
2052
+ preview: {
2053
+ select: { title: "title", cards: "cards" },
2054
+ prepare: ({ title, cards }) => ({
2055
+ title: title ?? "Feature cards",
2056
+ subtitle: \`\${cards?.length ?? 0} cards\`,
2057
+ }),
2058
+ },
2059
+ });
2060
+
2061
+ const imageLinkCard = defineArrayMember({
2062
+ name: "imageLinkCard",
2063
+ type: "object",
2064
+ fields: [
2065
+ defineField({ name: "title", type: "string", validation: (rule) => rule.required() }),
2066
+ defineField({ name: "description", type: "text", rows: 2 }),
2067
+ imageField,
2068
+ defineField({ name: "url", title: "Link", type: "customUrl" }),
2069
+ ],
2070
+ preview: { select: { title: "title", subtitle: "description", media: "image" } },
2071
+ });
2072
+
2073
+ export const imageLinkCards = defineType({
2074
+ name: "imageLinkCards",
2075
+ title: "Image link cards",
2076
+ type: "object",
2077
+ fields: [
2078
+ defineField({ name: "eyebrow", type: "string" }),
2079
+ defineField({ name: "title", type: "string", validation: (rule) => rule.required() }),
2080
+ defineField({ name: "richText", type: "richText" }),
2081
+ buttonsField,
2082
+ defineField({ name: "cards", type: "array", of: [imageLinkCard] }),
2083
+ ],
2084
+ preview: {
2085
+ select: { title: "title", cards: "cards" },
2086
+ prepare: ({ title, cards }) => ({
2087
+ title: title ?? "Image link cards",
2088
+ subtitle: \`\${cards?.length ?? 0} cards\`,
2089
+ }),
2090
+ },
2091
+ });
2092
+
2093
+ export const faqAccordion = defineType({
2094
+ name: "faqAccordion",
2095
+ title: "FAQ accordion",
2096
+ type: "object",
2097
+ fields: [
2098
+ defineField({ name: "eyebrow", type: "string" }),
2099
+ defineField({ name: "title", type: "string", validation: (rule) => rule.required() }),
2100
+ defineField({ name: "subtitle", type: "string" }),
2101
+ defineField({
2102
+ name: "faqs",
2103
+ title: "FAQs",
2104
+ type: "array",
2105
+ of: [defineArrayMember({ type: "reference", to: [{ type: "faq" }] })],
2106
+ validation: (rule) => rule.unique(),
2107
+ }),
2108
+ ],
2109
+ preview: {
2110
+ select: { title: "title", faqs: "faqs" },
2111
+ prepare: ({ title, faqs }) => ({
2112
+ title: title ?? "FAQ accordion",
2113
+ subtitle: \`\${faqs?.length ?? 0} questions\`,
2114
+ }),
2115
+ },
2116
+ });
2117
+
2118
+ export const subscribeNewsletter = defineType({
2119
+ name: "subscribeNewsletter",
2120
+ title: "Subscribe newsletter",
2121
+ type: "object",
2122
+ fields: [
2123
+ defineField({ name: "title", type: "string" }),
2124
+ defineField({ name: "subTitle", title: "Subtitle", type: "richText" }),
2125
+ defineField({ name: "helperText", title: "Helper text", type: "richText" }),
2126
+ ],
2127
+ preview: {
2128
+ select: { title: "title" },
2129
+ prepare: ({ title }) => ({ title: title ?? "Newsletter", subtitle: "Newsletter" }),
2130
+ },
2131
+ });
2132
+
2133
+ /**
2134
+ * Every page-builder block, in the order the \`page\` document offers them. The Studio
2135
+ * imports this and composes it with its own document schemas — one list to keep in step
2136
+ * with the renderers that live beside it, instead of two.
2137
+ */
2138
+ export const blockSchemas = [
2139
+ hero,
2140
+ richTextBlock,
2141
+ featureCardsIcon,
2142
+ imageLinkCards,
2143
+ faqAccordion,
2144
+ subscribeNewsletter,
2145
+ cta,
2146
+ ];
2147
+ `, SCHEMA_PAGE = `import { defineArrayMember, defineField, defineType } from "sanity";
2148
+
2149
+ export const page = defineType({
2150
+ name: "page",
2151
+ title: "Page",
2152
+ type: "document",
2153
+ fields: [
2154
+ defineField({ name: "title", type: "string", validation: (rule) => rule.required() }),
2155
+ defineField({
2156
+ name: "slug",
2157
+ type: "slug",
2158
+ options: { source: "title" },
2159
+ validation: (rule) => rule.required(),
2160
+ }),
2161
+ defineField({ name: "description", type: "text", rows: 3 }),
2162
+ defineField({ name: "image", type: "image", options: { hotspot: true } }),
2163
+ defineField({
2164
+ name: "pageBuilder",
2165
+ title: "Page builder",
2166
+ type: "array",
2167
+ of: [
2168
+ defineArrayMember({ type: "hero" }),
2169
+ defineArrayMember({ type: "richTextBlock" }),
2170
+ defineArrayMember({ type: "featureCardsIcon" }),
2171
+ defineArrayMember({ type: "imageLinkCards" }),
2172
+ defineArrayMember({ type: "faqAccordion" }),
2173
+ defineArrayMember({ type: "subscribeNewsletter" }),
2174
+ defineArrayMember({ type: "cta" }),
2175
+ ],
2176
+ }),
2177
+ ],
2178
+ // The home page is the \`page\` whose slug is "home" — see sanity/queries.ts.
2179
+ preview: { select: { title: "title", subtitle: "slug.current", media: "image" } },
2180
+ });
2181
+ `, SCHEMA_BLOG = `import { defineArrayMember, defineField, defineType } from "sanity";
2182
+
2183
+ export const author = defineType({
2184
+ name: "author",
2185
+ title: "Author",
2186
+ type: "document",
2187
+ fields: [
2188
+ defineField({ name: "name", type: "string", validation: (rule) => rule.required() }),
2189
+ defineField({ name: "position", type: "string" }),
2190
+ defineField({ name: "image", type: "image", options: { hotspot: true } }),
2191
+ defineField({ name: "bio", type: "text", rows: 3 }),
2192
+ ],
2193
+ preview: { select: { title: "name", subtitle: "position", media: "image" } },
2194
+ });
2195
+
2196
+ export const blog = defineType({
2197
+ name: "blog",
2198
+ title: "Blog post",
2199
+ type: "document",
2200
+ fields: [
2201
+ defineField({ name: "title", type: "string", validation: (rule) => rule.required() }),
2202
+ defineField({
2203
+ name: "slug",
2204
+ type: "slug",
2205
+ options: { source: "title" },
2206
+ validation: (rule) => rule.required(),
2207
+ }),
2208
+ defineField({ name: "description", type: "text", rows: 3 }),
2209
+ defineField({
2210
+ name: "authors",
2211
+ type: "array",
2212
+ of: [
2213
+ defineArrayMember({
2214
+ type: "reference",
2215
+ to: [{ type: "author" }],
2216
+ options: { disableNew: true },
2217
+ }),
2218
+ ],
2219
+ validation: (rule) => rule.unique().max(1),
2220
+ }),
2221
+ defineField({
2222
+ name: "publishedAt",
2223
+ title: "Published at",
2224
+ type: "date",
2225
+ // slice, not split("T")[0] — under noUncheckedIndexedAccess the index gives
2226
+ // \`string | undefined\` and initialValue will not take it.
2227
+ initialValue: () => new Date().toISOString().slice(0, 10),
2228
+ }),
2229
+ // Optional, unlike the reference's required image: seeded posts ship without image
2230
+ // assets, and the renderers have to handle a post with no image regardless.
2231
+ defineField({
2232
+ name: "image",
2233
+ type: "image",
2234
+ options: { hotspot: true },
2235
+ fields: [defineField({ name: "alt", title: "Alt text", type: "string" })],
2236
+ }),
2237
+ defineField({ name: "richText", title: "Content", type: "richText" }),
2238
+ ],
2239
+ preview: {
2240
+ select: { title: "title", author: "authors.0.name", date: "publishedAt", media: "image" },
2241
+ prepare: ({ title, author, date, media }) => ({
2242
+ title: title ?? "Untitled post",
2243
+ subtitle: [author, date].filter(Boolean).join(" · "),
2244
+ media,
2245
+ }),
2246
+ },
2247
+ });
2248
+
2249
+ export const faq = defineType({
2250
+ name: "faq",
2251
+ title: "FAQ",
2252
+ type: "document",
2253
+ fields: [
2254
+ defineField({
2255
+ name: "title",
2256
+ title: "Question",
2257
+ type: "string",
2258
+ validation: (rule) => rule.required(),
2259
+ }),
2260
+ defineField({ name: "richText", title: "Answer", type: "richText" }),
2261
+ ],
2262
+ preview: { select: { title: "title" } },
2263
+ });
2264
+ `, SCHEMA_SETTINGS = `import { defineField, defineType } from "sanity";
2265
+
2266
+ export const settings = defineType({
2267
+ name: "settings",
2268
+ title: "Settings",
2269
+ type: "document",
2270
+ fields: [
2271
+ defineField({ name: "siteTitle", type: "string", validation: (rule) => rule.required() }),
2272
+ defineField({ name: "siteDescription", type: "text", rows: 3 }),
2273
+ defineField({
2274
+ name: "socialImage",
2275
+ title: "Social share image",
2276
+ type: "image",
2277
+ options: { hotspot: true },
2278
+ }),
2279
+ ],
2280
+ preview: { select: { title: "siteTitle", media: "socialImage" } },
2281
+ });
2282
+ `, SCHEMA_INDEX = `import { blockSchemas } from "@workspace/sanity-blocks";
2283
+ import type { SchemaTypeDefinition } from "sanity";
2284
+ import { author, blog, faq } from "./blog";
2285
+ import { button, customUrl, richText } from "./definitions";
2286
+ import { page } from "./page";
2287
+ import { settings } from "./settings";
2288
+
2289
+ export const schemaTypes: SchemaTypeDefinition[] = [
2290
+ // Documents
2291
+ page,
2292
+ blog,
2293
+ author,
2294
+ faq,
2295
+ settings,
2296
+ // Reusable definitions
2297
+ customUrl,
2298
+ button,
2299
+ richText,
2300
+ // Page-builder blocks — declared in @workspace/sanity-blocks, next to their renderers.
2301
+ ...blockSchemas,
2302
+ ];
2303
+ `, STRUCTURE = `import type { StructureResolver } from "sanity/structure";
2304
+
2305
+ /**
2306
+ * \`settings\` is a singleton: one document with a fixed id, edited in place instead of
2307
+ * listed. Everything else falls through to the default document-type lists.
1388
2308
  *
1389
- * Without credentials it returns null before touching the network, so \`astro build\`
1390
- * succeeds on a fresh scaffold. The catch covers a configured-but-unreachable dataset
1391
- * for the same reason: a starter should render empty, not crash.
2309
+ * The two sets feed sanity.config.ts's \`document.newDocumentOptions\` / \`document.actions\`
2310
+ * guards (the official singleton recipe), so a second settings document cannot be created.
2311
+ */
2312
+ export const singletonTypes = new Set(["settings"]);
2313
+
2314
+ /** Actions that keep a singleton editable without allowing create/delete/duplicate. */
2315
+ export const singletonActions = new Set(["publish", "discardChanges", "restore"]);
2316
+
2317
+ export const structure: StructureResolver = (S) =>
2318
+ S.list()
2319
+ .title("Content")
2320
+ .items([
2321
+ S.listItem()
2322
+ .title("Settings")
2323
+ .id("settings")
2324
+ .child(S.document().schemaType("settings").documentId("settings")),
2325
+ S.divider(),
2326
+ S.documentTypeListItem("page").title("Pages"),
2327
+ S.documentTypeListItem("blog").title("Blog posts"),
2328
+ S.documentTypeListItem("author").title("Authors"),
2329
+ S.documentTypeListItem("faq").title("FAQs"),
2330
+ ]);
2331
+ `, PRESENTATION = `import { defineDocuments, defineLocations } from "sanity/presentation";
2332
+
2333
+ /** Route → document: lets Presentation open the right document for the URL in the iframe. */
2334
+ export const mainDocuments = defineDocuments([
2335
+ { route: "/", filter: \`_type == "page" && slug.current == "home"\` },
2336
+ { route: "/blog/:slug", filter: \`_type == "blog" && slug.current == $slug\` },
2337
+ { route: "/:slug", filter: \`_type == "page" && slug.current == $slug\` },
2338
+ ]);
2339
+
2340
+ /** Document → routes: the "Used on …" links shown when editing a document. */
2341
+ export const locations = {
2342
+ settings: defineLocations({
2343
+ locations: [{ title: "Home", href: "/" }],
2344
+ message: "This document is used on all pages",
2345
+ tone: "positive",
2346
+ }),
2347
+ page: defineLocations({
2348
+ select: { title: "title", slug: "slug.current" },
2349
+ resolve: (doc) => ({
2350
+ locations: [
2351
+ {
2352
+ title: doc?.title || "Untitled",
2353
+ href: doc?.slug === "home" ? "/" : \`/\${doc?.slug}\`,
2354
+ },
2355
+ ],
2356
+ }),
2357
+ }),
2358
+ blog: defineLocations({
2359
+ select: { title: "title", slug: "slug.current" },
2360
+ resolve: (doc) => ({
2361
+ locations: [
2362
+ { title: doc?.title || "Untitled", href: \`/blog/\${doc?.slug}\` },
2363
+ { title: "Blog", href: "/blog" },
2364
+ ],
2365
+ }),
2366
+ }),
2367
+ };
2368
+ `, DOCUMENT_GUARD = ` document: {
2369
+ newDocumentOptions: (prev, { creationContext }) =>
2370
+ creationContext.type === "global"
2371
+ ? prev.filter((template) => !singletonTypes.has(template.templateId))
2372
+ : prev,
2373
+ actions: (prev, { schemaType }) =>
2374
+ singletonTypes.has(schemaType)
2375
+ ? prev.filter(({ action }) => action && singletonActions.has(action))
2376
+ : prev,
2377
+ },`, QUERIES = `import { defineQuery } from "groq";
2378
+
2379
+ /**
2380
+ * A \`customUrl\` collapsed to a plain href, so components never branch on internal vs
2381
+ * external. Written against a field literally named \`url\` — the shape \`button\` and
2382
+ * \`imageLinkCard\` both use.
1392
2383
  */
1393
- export async function loadQuery<T>(
1394
- query: string,
1395
- params: Record<string, unknown> = {},
1396
- ): Promise<T | null> {
1397
- if (!hasSanityCredentials) return null;
1398
- if (visualEditingEnabled && !readToken) {
1399
- throw new Error(
1400
- "SANITY_API_READ_TOKEN is required when PUBLIC_SANITY_VISUAL_EDITING_ENABLED is true.",
1401
- );
1402
- }
1403
- try {
1404
- const { result } = await sanityClient.fetch<T>(query, params, {
1405
- filterResponse: false,
1406
- perspective: visualEditingEnabled ? "drafts" : "published",
1407
- resultSourceMap: visualEditingEnabled ? "withKeyArraySelector" : false,
1408
- stega: visualEditingEnabled,
1409
- ...(visualEditingEnabled ? { token: readToken } : {}),
1410
- useCdn: !visualEditingEnabled,
1411
- });
1412
- return result;
1413
- } catch (error) {
1414
- console.warn("[sanity] query failed:", error);
1415
- return null;
2384
+ const LINK_FIELDS = /* groq */ \`
2385
+ "openInNewTab": url.openInNewTab,
2386
+ "href": select(
2387
+ url.type == "external" => url.external,
2388
+ url.internal->_type == "blog" => "/blog/" + url.internal->slug.current,
2389
+ url.internal->slug.current == "home" => "/",
2390
+ defined(url.internal->slug.current) => "/" + url.internal->slug.current,
2391
+ "#"
2392
+ )
2393
+ \`;
2394
+
2395
+ const BUTTONS = /* groq */ \`buttons[]{ _key, _type, text, variant, \${LINK_FIELDS} }\`;
2396
+
2397
+ // \`...\` first, then the overrides: every block keeps its plain fields, and the two that
2398
+ // hold references or links get resolved on top.
2399
+ const PAGE_BUILDER = /* groq */ \`
2400
+ pageBuilder[]{
2401
+ ...,
2402
+ \${BUTTONS},
2403
+ _type == "faqAccordion" => { faqs[]->{ _id, title, richText } },
2404
+ _type == "imageLinkCards" => { cards[]{ ..., \${LINK_FIELDS} } }
1416
2405
  }
1417
- }
1418
- `, ASTRO_IMAGE = `import { createImageUrlBuilder } from "@sanity/image-url";
1419
- import { dataset, projectId } from "./env";
2406
+ \`;
2407
+
2408
+ const PAGE_FIELDS = /* groq */ \`
2409
+ _id,
2410
+ _type,
2411
+ title,
2412
+ description,
2413
+ "slug": slug.current,
2414
+ \${PAGE_BUILDER}
2415
+ \`;
2416
+
2417
+ const BLOG_CARD_FIELDS = /* groq */ \`
2418
+ _id,
2419
+ title,
2420
+ description,
2421
+ publishedAt,
2422
+ image,
2423
+ "slug": slug.current,
2424
+ "authors": authors[]->{ _id, name, position, image }
2425
+ \`;
2426
+
2427
+ export const HOME_PAGE_QUERY = defineQuery(
2428
+ \`*[_type == "page" && slug.current == "home"][0]{\${PAGE_FIELDS}}\`,
2429
+ );
2430
+
2431
+ export const PAGE_QUERY = defineQuery(
2432
+ \`*[_type == "page" && slug.current == $slug][0]{\${PAGE_FIELDS}}\`,
2433
+ );
2434
+
2435
+ export const PAGE_SLUGS_QUERY = defineQuery(
2436
+ \`*[_type == "page" && defined(slug.current) && slug.current != "home"].slug.current\`,
2437
+ );
2438
+
2439
+ export const BLOG_INDEX_QUERY = defineQuery(
2440
+ \`*[_type == "blog" && defined(slug.current)]|order(publishedAt desc){\${BLOG_CARD_FIELDS}}\`,
2441
+ );
2442
+
2443
+ export const BLOG_QUERY = defineQuery(
2444
+ \`*[_type == "blog" && slug.current == $slug][0]{\${BLOG_CARD_FIELDS}, richText}\`,
2445
+ );
2446
+
2447
+ export const BLOG_SLUGS_QUERY = defineQuery(
2448
+ \`*[_type == "blog" && defined(slug.current)].slug.current\`,
2449
+ );
2450
+
2451
+ export const SETTINGS_QUERY = defineQuery(
2452
+ \`*[_type == "settings"][0]{ siteTitle, siteDescription, socialImage }\`,
2453
+ );
2454
+ `;
2455
+
2456
+ // ../../packages/registry/src/content/sanity/seed.ts
2457
+ var para = (key, text) => ({
2458
+ _key: key,
2459
+ _type: "block",
2460
+ style: "normal",
2461
+ markDefs: [],
2462
+ children: [{ _key: `${key}s`, _type: "span", marks: [], text }]
2463
+ }), internal = (id) => ({
2464
+ _type: "customUrl",
2465
+ type: "internal",
2466
+ openInNewTab: false,
2467
+ internal: { _type: "reference", _ref: id }
2468
+ }), external = (href) => ({
2469
+ _type: "customUrl",
2470
+ type: "external",
2471
+ openInNewTab: true,
2472
+ external: href
2473
+ }), button = (key, text, url, variant = "default") => ({
2474
+ _key: key,
2475
+ _type: "button",
2476
+ text,
2477
+ variant,
2478
+ url
2479
+ }), DOCS, SEED_NDJSON;
2480
+ var init_seed = __esm(() => {
2481
+ DOCS = [
2482
+ {
2483
+ _id: "settings",
2484
+ _type: "settings",
2485
+ siteTitle: "Turbo Wizard",
2486
+ siteDescription: "A Turborepo monorepo scaffolded with create-turbo-wizard, with Sanity wired end to end."
2487
+ },
2488
+ {
2489
+ _id: "author-nova",
2490
+ _type: "author",
2491
+ name: "Nova Lang",
2492
+ position: "Staff engineer",
2493
+ bio: "Writes about build systems and the parts of a monorepo nobody reads until they break."
2494
+ },
2495
+ {
2496
+ _id: "author-atlas",
2497
+ _type: "author",
2498
+ name: "Atlas Reyes",
2499
+ position: "Content lead",
2500
+ bio: "Turns a schema into something an editor can actually use."
2501
+ },
2502
+ {
2503
+ _id: "faq-content",
2504
+ _type: "faq",
2505
+ title: "Where does this content live?",
2506
+ richText: [
2507
+ para("a1", "In your Sanity dataset. The document schemas are in apps/studio/schemaTypes, the queries are in packages/sanity/src/queries.ts, and the Studio edits what they describe.")
2508
+ ]
2509
+ },
2510
+ {
2511
+ _id: "faq-blocks",
2512
+ _type: "faq",
2513
+ title: "How do I add a page-builder block?",
2514
+ richText: [
2515
+ para("a2", "Define it in packages/sanity-blocks/src/schema.ts, add it to the page document's pageBuilder array, then render it in the page-builder component beside it. Three files, no engine changes.")
2516
+ ]
2517
+ },
2518
+ {
2519
+ _id: "faq-credentials",
2520
+ _type: "faq",
2521
+ title: "What happens without a project id?",
2522
+ richText: [
2523
+ para("a3", "Every read short-circuits and the site renders an empty state. The build never dials the API, so a fresh scaffold ships green before anyone signs up for anything.")
2524
+ ]
2525
+ },
2526
+ {
2527
+ _id: "page-home",
2528
+ _type: "page",
2529
+ title: "Turbo Wizard",
2530
+ description: "The home page, served from Sanity.",
2531
+ slug: { _type: "slug", current: "home" },
2532
+ pageBuilder: [
2533
+ {
2534
+ _key: "hero",
2535
+ _type: "hero",
2536
+ badge: "Scaffolded",
2537
+ title: "Content from Sanity, rendered by your app",
2538
+ richText: [
2539
+ para("h1", "This page lives in the Studio. Edit it there and it changes here — every block below came out of the same dataset.")
2540
+ ],
2541
+ buttons: [
2542
+ button("hb1", "Read the blog", internal("blog-page-builder")),
2543
+ button("hb2", "Sanity docs", external("https://www.sanity.io/docs"), "outline")
2544
+ ]
2545
+ },
2546
+ {
2547
+ _key: "features",
2548
+ _type: "featureCardsIcon",
2549
+ eyebrow: "What you got",
2550
+ title: "Wired end to end",
2551
+ cards: [
2552
+ {
2553
+ _key: "f1",
2554
+ _type: "featureCardIcon",
2555
+ icon: "layers",
2556
+ title: "Page builder",
2557
+ richText: [para("f1a", "Seven block types, composed per page in the Studio.")]
2558
+ },
2559
+ {
2560
+ _key: "f2",
2561
+ _type: "featureCardIcon",
2562
+ icon: "pen-line",
2563
+ title: "Blog",
2564
+ richText: [para("f2a", "Posts with authors, dates and Portable Text bodies.")]
2565
+ },
2566
+ {
2567
+ _key: "f3",
2568
+ _type: "featureCardIcon",
2569
+ icon: "shield-check",
2570
+ title: "Typed queries",
2571
+ richText: [para("f3a", "defineQuery plus typegen, so results are not any.")]
2572
+ }
2573
+ ]
2574
+ },
2575
+ {
2576
+ _key: "cards",
2577
+ _type: "imageLinkCards",
2578
+ eyebrow: "Go deeper",
2579
+ title: "Where to look next",
2580
+ cards: [
2581
+ {
2582
+ _key: "c1",
2583
+ _type: "imageLinkCard",
2584
+ title: "The schema",
2585
+ description: "Documents, blocks and the reusable definitions they share.",
2586
+ url: external("https://www.sanity.io/docs/schema-types")
2587
+ },
2588
+ {
2589
+ _key: "c2",
2590
+ _type: "imageLinkCard",
2591
+ title: "About this starter",
2592
+ description: "A second page, routed by its slug.",
2593
+ url: internal("page-about")
2594
+ }
2595
+ ]
2596
+ },
2597
+ {
2598
+ _key: "faqs",
2599
+ _type: "faqAccordion",
2600
+ eyebrow: "Questions",
2601
+ title: "The ones that come up first",
2602
+ faqs: [
2603
+ { _key: "q1", _type: "reference", _ref: "faq-content" },
2604
+ { _key: "q2", _type: "reference", _ref: "faq-blocks" },
2605
+ { _key: "q3", _type: "reference", _ref: "faq-credentials" }
2606
+ ]
2607
+ },
2608
+ {
2609
+ _key: "prose",
2610
+ _type: "richTextBlock",
2611
+ eyebrow: "How it fits together",
2612
+ title: "One schema, two frontends",
2613
+ richText: [
2614
+ para("p1", "The schema and the GROQ are frontend-agnostic — the same files ship whether you picked Next or Astro. Only the rendering layer differs."),
2615
+ para("p2", "The page builder renders each block by its _type and skips anything it does not recognise, so adding a block to the schema never breaks a deployed page.")
2616
+ ]
2617
+ },
2618
+ {
2619
+ _key: "newsletter",
2620
+ _type: "subscribeNewsletter",
2621
+ title: "Ship notes",
2622
+ subTitle: [para("n1", "Occasional posts about the stack this project scaffolds.")],
2623
+ helperText: [para("n2", "No spam. The form is a stub until you wire it up.")]
2624
+ },
2625
+ {
2626
+ _key: "cta",
2627
+ _type: "cta",
2628
+ eyebrow: "Your turn",
2629
+ title: "Open the Studio",
2630
+ richText: [
2631
+ para("ct1", "Edit this page in the Studio — pnpm dev:studio — then publish and reload.")
2632
+ ],
2633
+ buttons: [button("cb1", "Read the blog", internal("blog-typed-groq"))]
2634
+ }
2635
+ ]
2636
+ },
2637
+ {
2638
+ _id: "page-about",
2639
+ _type: "page",
2640
+ title: "About",
2641
+ description: "A second page, to show routing by slug.",
2642
+ slug: { _type: "slug", current: "about" },
2643
+ pageBuilder: [
2644
+ {
2645
+ _key: "hero",
2646
+ _type: "hero",
2647
+ title: "About this starter",
2648
+ richText: [para("ab1", "Pages are routed by their slug. This one is /about.")]
2649
+ },
2650
+ {
2651
+ _key: "prose",
2652
+ _type: "richTextBlock",
2653
+ title: "Add your own",
2654
+ richText: [
2655
+ para("ab2", "Create a page in the Studio, give it a slug, publish it, and it appears at that path on the next build.")
2656
+ ]
2657
+ }
2658
+ ]
2659
+ },
2660
+ {
2661
+ _id: "blog-page-builder",
2662
+ _type: "blog",
2663
+ title: "A page builder is just an array",
2664
+ description: "The page-builder pattern in one sentence: a document holds an array of objects, and the frontend renders each one by its _type.",
2665
+ slug: { _type: "slug", current: "a-page-builder-is-just-an-array" },
2666
+ publishedAt: "2026-01-14",
2667
+ authors: [{ _key: "a1", _type: "reference", _ref: "author-nova" }],
2668
+ richText: [
2669
+ para("b1", "Every block in this starter is a Sanity object type. The page document holds them in an ordered array, and the renderer switches on _type — unknown types are skipped rather than thrown on, so a schema change can never take down a live page."),
2670
+ para("b2", "The interesting consequence is that adding a block is a schema edit plus a component, with no routing and no migration. Editors compose; developers supply the vocabulary.")
2671
+ ]
2672
+ },
2673
+ {
2674
+ _id: "blog-typed-groq",
2675
+ _type: "blog",
2676
+ title: "Typed GROQ, without hand-writing types",
2677
+ description: "defineQuery marks a string as a query, sanity typegen reads the schema, and query results stop being any.",
2678
+ slug: { _type: "slug", current: "typed-groq" },
2679
+ publishedAt: "2026-01-28",
2680
+ authors: [{ _key: "a1", _type: "reference", _ref: "author-nova" }],
2681
+ richText: [
2682
+ para("b1", "Wrapping a query in defineQuery is what makes it visible to typegen. A plain template literal looks like any other string and its result stays any, which is the failure mode you notice three refactors later."),
2683
+ para("b2", "This project commits the generated types, so a fresh clone type-checks before anything runs; re-run extract and typegen generate from apps/studio after a schema or query edit.")
2684
+ ]
2685
+ },
2686
+ {
2687
+ _id: "blog-editors",
2688
+ _type: "blog",
2689
+ title: "Write the schema for the person editing it",
2690
+ description: "Field descriptions, previews and singletons are not polish. They are the difference between a Studio someone uses and one they avoid.",
2691
+ slug: { _type: "slug", current: "write-the-schema-for-the-editor" },
2692
+ publishedAt: "2026-02-09",
2693
+ authors: [{ _key: "a1", _type: "reference", _ref: "author-atlas" }],
2694
+ richText: [
2695
+ para("b1", "A document list showing three rows of Untitled is a schema that forgot it has users. Previews take a prepare function; use it to show the slug, the author, the publish date — whatever tells one row from another."),
2696
+ para("b2", "The same goes for singletons. Settings should be one document you open, not a list you can accidentally add a second row to.")
2697
+ ]
2698
+ }
2699
+ ];
2700
+ SEED_NDJSON = `${DOCS.map((doc) => JSON.stringify(doc)).join(`
2701
+ `)}
2702
+ `;
2703
+ });
2704
+
2705
+ // ../../packages/registry/src/content/sanity/studio.ts
2706
+ var j5 = (value) => `${JSON.stringify(value, null, 2)}
2707
+ `, STUDIO_PACKAGE_JSON, STUDIO_TSCONFIG, NEXT_SITE_URL = "http://localhost:3000", ASTRO_SITE_URL = "http://localhost:4321", studioEnvExample = (siteUrl) => `# Copy to apps/studio/.env, then fill in the project id.
2708
+ # A Studio only reads SANITY_STUDIO_*-prefixed variables.
2709
+ SANITY_STUDIO_PROJECT_ID=
2710
+ SANITY_STUDIO_DATASET=
2711
+ # Where Presentation points its preview iframe (the site, not the Studio).
2712
+ SANITY_STUDIO_PRESENTATION_URL=${siteUrl}
2713
+ `, STUDIO_ENV_EXAMPLE_NEXT, STUDIO_ENV_EXAMPLE_ASTRO, STUDIO_CLI = `import { defineCliConfig } from "sanity/cli";
2714
+
2715
+ export default defineCliConfig({
2716
+ api: {
2717
+ projectId: process.env.SANITY_STUDIO_PROJECT_ID || "placeholder",
2718
+ dataset: process.env.SANITY_STUDIO_DATASET || "production",
2719
+ },
2720
+ // The Studio owns extraction and typegen — the CLI has to run where sanity.config.ts
2721
+ // lives — but the generated types land beside the queries they type, in
2722
+ // @workspace/sanity. Paths are relative to apps/studio; \`schema\` defaults to the
2723
+ // schema.json that \`sanity schema extract\` writes here.
2724
+ typegen: {
2725
+ path: "../../packages/sanity/src/**/*.{ts,tsx}",
2726
+ generates: "../../packages/sanity/src/sanity.types.ts",
2727
+ },
2728
+ });
2729
+ `, studioConfig = (siteUrl, previewMode) => `import { defineConfig } from "sanity";
2730
+ import { presentationTool } from "sanity/presentation";
2731
+ import { structureTool } from "sanity/structure";
2732
+ import { locations, mainDocuments } from "./presentation";
2733
+ import { schemaTypes } from "./schemaTypes/index";
2734
+ import { singletonActions, singletonTypes, structure } from "./structure";
2735
+
2736
+ export default defineConfig({
2737
+ name: "default",
2738
+ title: "Studio",
2739
+ projectId: process.env.SANITY_STUDIO_PROJECT_ID || "placeholder",
2740
+ dataset: process.env.SANITY_STUDIO_DATASET || "production",
2741
+ plugins: [
2742
+ presentationTool({
2743
+ resolve: { mainDocuments, locations },
2744
+ previewUrl: {
2745
+ // Absolute, unlike an embedded Studio's "/": the site is a different origin now.
2746
+ initial: process.env.SANITY_STUDIO_PRESENTATION_URL || "${siteUrl}",${previewMode}
2747
+ },
2748
+ }),
2749
+ structureTool({ structure }),
2750
+ ],
2751
+ schema: { types: schemaTypes },
2752
+ ${DOCUMENT_GUARD}
2753
+ });
2754
+ `, STUDIO_CONFIG_NEXT, STUDIO_CONFIG_ASTRO;
2755
+ var init_studio = __esm(() => {
2756
+ STUDIO_PACKAGE_JSON = j5({
2757
+ name: "studio",
2758
+ version: "0.0.0",
2759
+ private: true,
2760
+ type: "module",
2761
+ scripts: {
2762
+ build: "sanity build",
2763
+ "check-types": "tsc --noEmit",
2764
+ dev: "sanity dev",
2765
+ extract: "sanity schema extract --enforce-required-fields",
2766
+ "sanity:seed": "sanity dataset import seed.ndjson",
2767
+ type: "sanity typegen generate"
2768
+ },
2769
+ dependencies: {
2770
+ "@workspace/sanity-blocks": "workspace:*",
2771
+ react: "^19.0.0",
2772
+ "react-dom": "^19.0.0",
2773
+ sanity: "^5.31.1",
2774
+ "styled-components": "^6.1.15"
2775
+ },
2776
+ devDependencies: {
2777
+ "@types/node": "^22.10.0",
2778
+ "@types/react": "^19.0.0",
2779
+ "@workspace/typescript-config": "workspace:*",
2780
+ typescript: "^5.9.0"
2781
+ }
2782
+ });
2783
+ STUDIO_TSCONFIG = j5({
2784
+ extends: "@workspace/typescript-config/react-library.json",
2785
+ include: ["**/*.ts", "**/*.tsx"],
2786
+ exclude: ["node_modules", "dist"]
2787
+ });
2788
+ STUDIO_ENV_EXAMPLE_NEXT = studioEnvExample(NEXT_SITE_URL);
2789
+ STUDIO_ENV_EXAMPLE_ASTRO = studioEnvExample(ASTRO_SITE_URL);
2790
+ STUDIO_CONFIG_NEXT = studioConfig(NEXT_SITE_URL, `
2791
+ previewMode: {
2792
+ enable: "/api/draft-mode/enable",
2793
+ // Presentation never calls this itself (documented as not yet implemented);
2794
+ // editors leave draft mode via <DisableDraftMode/>, which links to the route.
2795
+ disable: "/api/draft-mode/disable",
2796
+ },`);
2797
+ STUDIO_CONFIG_ASTRO = studioConfig(ASTRO_SITE_URL, `
2798
+ // No previewMode: Astro has no draft-mode routes. Draft content comes from
2799
+ // loadQuery's perspective switch (PUBLIC_SANITY_VISUAL_EDITING_ENABLED + a read
2800
+ // token), which needs a rendering server — \`astro dev\`, or an SSR adapter.`);
2801
+ });
2802
+
2803
+ // ../../packages/registry/src/content/sanity/types.ts
2804
+ var SANITY_TYPES = `/**
2805
+ * ---------------------------------------------------------------------------------
2806
+ * This file has been generated by Sanity TypeGen.
2807
+ * Command: \`sanity typegen generate\`
2808
+ *
2809
+ * Any modifications made directly to this file will be overwritten the next time
2810
+ * the TypeScript definitions are generated. Please make changes to the Sanity
2811
+ * schema definitions and/or GROQ queries if you need to update these types.
2812
+ *
2813
+ * For more information on how to use Sanity TypeGen, visit the official documentation:
2814
+ * https://www.sanity.io/docs/sanity-typegen
2815
+ * ---------------------------------------------------------------------------------
2816
+ */
2817
+
2818
+ export declare const internalGroqTypeReferenceTo: unique symbol;
2819
+
2820
+ // Source: schema.json
2821
+ export type SanityImageAssetReference = {
2822
+ _ref: string;
2823
+ _type: "reference";
2824
+ _weak?: boolean;
2825
+ [internalGroqTypeReferenceTo]?: "sanity.imageAsset";
2826
+ };
2827
+
2828
+ export type Image1 = {
2829
+ asset?: SanityImageAssetReference;
2830
+ media?: unknown; // Unable to locate the referenced type "image.media" in schema
2831
+ hotspot?: SanityImageHotspot;
2832
+ crop?: SanityImageCrop;
2833
+ alt?: string;
2834
+ _type: "image";
2835
+ };
2836
+
2837
+ export type Cta = {
2838
+ _type: "cta";
2839
+ eyebrow?: string;
2840
+ title?: string;
2841
+ richText?: RichText;
2842
+ buttons?: Array<
2843
+ {
2844
+ _key: string;
2845
+ } & Button
2846
+ >;
2847
+ };
2848
+
2849
+ export type SubscribeNewsletter = {
2850
+ _type: "subscribeNewsletter";
2851
+ title?: string;
2852
+ subTitle?: RichText;
2853
+ helperText?: RichText;
2854
+ };
2855
+
2856
+ export type FaqReference = {
2857
+ _ref: string;
2858
+ _type: "reference";
2859
+ _weak?: boolean;
2860
+ [internalGroqTypeReferenceTo]?: "faq";
2861
+ };
2862
+
2863
+ export type FaqAccordion = {
2864
+ _type: "faqAccordion";
2865
+ eyebrow?: string;
2866
+ title: string;
2867
+ subtitle?: string;
2868
+ faqs?: Array<
2869
+ {
2870
+ _key: string;
2871
+ } & FaqReference
2872
+ >;
2873
+ };
2874
+
2875
+ export type ImageLinkCards = {
2876
+ _type: "imageLinkCards";
2877
+ eyebrow?: string;
2878
+ title: string;
2879
+ richText?: RichText;
2880
+ buttons?: Array<
2881
+ {
2882
+ _key: string;
2883
+ } & Button
2884
+ >;
2885
+ cards?: Array<{
2886
+ title: string;
2887
+ description?: string;
2888
+ image?: Image1;
2889
+ url?: CustomUrl;
2890
+ _type: "imageLinkCard";
2891
+ _key: string;
2892
+ }>;
2893
+ };
2894
+
2895
+ export type FeatureCardsIcon = {
2896
+ _type: "featureCardsIcon";
2897
+ eyebrow?: string;
2898
+ title?: string;
2899
+ richText?: RichText;
2900
+ cards?: Array<{
2901
+ icon?: string;
2902
+ title?: string;
2903
+ richText?: RichText;
2904
+ _type: "featureCardIcon";
2905
+ _key: string;
2906
+ }>;
2907
+ };
2908
+
2909
+ export type RichTextBlock = {
2910
+ _type: "richTextBlock";
2911
+ eyebrow?: string;
2912
+ title?: string;
2913
+ richText?: RichText;
2914
+ };
2915
+
2916
+ export type Hero = {
2917
+ _type: "hero";
2918
+ badge?: string;
2919
+ title?: string;
2920
+ richText?: RichText;
2921
+ image?: Image1;
2922
+ buttons?: Array<
2923
+ {
2924
+ _key: string;
2925
+ } & Button
2926
+ >;
2927
+ };
2928
+
2929
+ export type RichText = Array<
2930
+ | {
2931
+ children?: Array<{
2932
+ marks?: Array<string>;
2933
+ text?: string;
2934
+ _type: "span";
2935
+ _key: string;
2936
+ }>;
2937
+ style?: "normal" | "h2" | "h3" | "h4";
2938
+ listItem?: "bullet" | "number";
2939
+ markDefs?: Array<{
2940
+ customLink?: CustomUrl;
2941
+ _type: "customLink";
2942
+ _key: string;
2943
+ }>;
2944
+ level?: number;
2945
+ _type: "block";
2946
+ _key: string;
2947
+ }
2948
+ | {
2949
+ asset?: SanityImageAssetReference;
2950
+ media?: unknown;
2951
+ hotspot?: SanityImageHotspot;
2952
+ crop?: SanityImageCrop;
2953
+ caption?: string;
2954
+ _type: "image";
2955
+ _key: string;
2956
+ }
2957
+ >;
2958
+
2959
+ export type Button = {
2960
+ _type: "button";
2961
+ variant?: "default" | "secondary" | "outline" | "link";
2962
+ text?: string;
2963
+ url?: CustomUrl;
2964
+ };
2965
+
2966
+ export type PageReference = {
2967
+ _ref: string;
2968
+ _type: "reference";
2969
+ _weak?: boolean;
2970
+ [internalGroqTypeReferenceTo]?: "page";
2971
+ };
2972
+
2973
+ export type BlogReference = {
2974
+ _ref: string;
2975
+ _type: "reference";
2976
+ _weak?: boolean;
2977
+ [internalGroqTypeReferenceTo]?: "blog";
2978
+ };
2979
+
2980
+ export type CustomUrl = {
2981
+ _type: "customUrl";
2982
+ type: "internal" | "external";
2983
+ openInNewTab?: boolean;
2984
+ external?: string;
2985
+ internal?: PageReference | BlogReference;
2986
+ };
1420
2987
 
1421
- const builder = createImageUrlBuilder({ projectId, dataset });
2988
+ export type Settings = {
2989
+ _id: string;
2990
+ _type: "settings";
2991
+ _createdAt: string;
2992
+ _updatedAt: string;
2993
+ _rev: string;
2994
+ siteTitle: string;
2995
+ siteDescription?: string;
2996
+ socialImage?: {
2997
+ asset?: SanityImageAssetReference;
2998
+ media?: unknown;
2999
+ hotspot?: SanityImageHotspot;
3000
+ crop?: SanityImageCrop;
3001
+ _type: "image";
3002
+ };
3003
+ };
1422
3004
 
1423
- /** Build a CDN URL for a Sanity image; null when the block has no asset. */
1424
- export function urlFor(source: unknown, width = 1200): string | null {
1425
- const ref = (source as { asset?: { _ref?: string } } | null)?.asset?._ref;
1426
- if (!ref) return null;
1427
- return builder.image(ref).width(width).auto("format").url();
1428
- }
1429
- `, ASTRO_TYPES = `import type { PortableTextBlock } from "@portabletext/types";
3005
+ export type SanityImageCrop = {
3006
+ _type: "sanity.imageCrop";
3007
+ top: number;
3008
+ bottom: number;
3009
+ left: number;
3010
+ right: number;
3011
+ };
1430
3012
 
1431
- type SanityImage = { asset?: { _ref?: string } | null; alt?: string | null } | null;
3013
+ export type SanityImageHotspot = {
3014
+ _type: "sanity.imageHotspot";
3015
+ x: number;
3016
+ y: number;
3017
+ height: number;
3018
+ width: number;
3019
+ };
1432
3020
 
1433
- export type PageBlock =
1434
- | { _key: string; _type: "hero"; heading?: string; subheading?: string; image?: SanityImage }
1435
- | { _key: string; _type: "richText"; heading?: string; content?: PortableTextBlock[] }
1436
- | {
3021
+ export type Faq = {
3022
+ _id: string;
3023
+ _type: "faq";
3024
+ _createdAt: string;
3025
+ _updatedAt: string;
3026
+ _rev: string;
3027
+ title: string;
3028
+ richText?: RichText;
3029
+ };
3030
+
3031
+ export type Author = {
3032
+ _id: string;
3033
+ _type: "author";
3034
+ _createdAt: string;
3035
+ _updatedAt: string;
3036
+ _rev: string;
3037
+ name: string;
3038
+ position?: string;
3039
+ image?: {
3040
+ asset?: SanityImageAssetReference;
3041
+ media?: unknown;
3042
+ hotspot?: SanityImageHotspot;
3043
+ crop?: SanityImageCrop;
3044
+ _type: "image";
3045
+ };
3046
+ bio?: string;
3047
+ };
3048
+
3049
+ export type AuthorReference = {
3050
+ _ref: string;
3051
+ _type: "reference";
3052
+ _weak?: boolean;
3053
+ [internalGroqTypeReferenceTo]?: "author";
3054
+ };
3055
+
3056
+ export type Blog = {
3057
+ _id: string;
3058
+ _type: "blog";
3059
+ _createdAt: string;
3060
+ _updatedAt: string;
3061
+ _rev: string;
3062
+ title: string;
3063
+ slug: Slug;
3064
+ description?: string;
3065
+ authors?: Array<
3066
+ {
1437
3067
  _key: string;
1438
- _type: "cta";
1439
- heading?: string;
1440
- text?: string;
1441
- buttonLabel?: string;
1442
- buttonHref?: string;
1443
- };
3068
+ } & AuthorReference
3069
+ >;
3070
+ publishedAt?: string;
3071
+ image?: {
3072
+ asset?: SanityImageAssetReference;
3073
+ media?: unknown;
3074
+ hotspot?: SanityImageHotspot;
3075
+ crop?: SanityImageCrop;
3076
+ alt?: string;
3077
+ _type: "image";
3078
+ };
3079
+ richText?: RichText;
3080
+ };
3081
+
3082
+ export type Slug = {
3083
+ _type: "slug";
3084
+ current: string;
3085
+ source?: string;
3086
+ };
3087
+
3088
+ export type Page = {
3089
+ _id: string;
3090
+ _type: "page";
3091
+ _createdAt: string;
3092
+ _updatedAt: string;
3093
+ _rev: string;
3094
+ title: string;
3095
+ slug: Slug;
3096
+ description?: string;
3097
+ image?: {
3098
+ asset?: SanityImageAssetReference;
3099
+ media?: unknown;
3100
+ hotspot?: SanityImageHotspot;
3101
+ crop?: SanityImageCrop;
3102
+ _type: "image";
3103
+ };
3104
+ pageBuilder?: Array<
3105
+ | ({
3106
+ _key: string;
3107
+ } & Hero)
3108
+ | ({
3109
+ _key: string;
3110
+ } & RichTextBlock)
3111
+ | ({
3112
+ _key: string;
3113
+ } & FeatureCardsIcon)
3114
+ | ({
3115
+ _key: string;
3116
+ } & ImageLinkCards)
3117
+ | ({
3118
+ _key: string;
3119
+ } & FaqAccordion)
3120
+ | ({
3121
+ _key: string;
3122
+ } & SubscribeNewsletter)
3123
+ | ({
3124
+ _key: string;
3125
+ } & Cta)
3126
+ >;
3127
+ };
3128
+
3129
+ export type SanityImagePaletteSwatch = {
3130
+ _type: "sanity.imagePaletteSwatch";
3131
+ background?: string;
3132
+ foreground?: string;
3133
+ population?: number;
3134
+ title?: string;
3135
+ };
3136
+
3137
+ export type SanityImagePalette = {
3138
+ _type: "sanity.imagePalette";
3139
+ darkMuted?: SanityImagePaletteSwatch;
3140
+ lightVibrant?: SanityImagePaletteSwatch;
3141
+ darkVibrant?: SanityImagePaletteSwatch;
3142
+ vibrant?: SanityImagePaletteSwatch;
3143
+ dominant?: SanityImagePaletteSwatch;
3144
+ lightMuted?: SanityImagePaletteSwatch;
3145
+ muted?: SanityImagePaletteSwatch;
3146
+ };
3147
+
3148
+ export type SanityImageDimensions = {
3149
+ _type: "sanity.imageDimensions";
3150
+ height: number;
3151
+ width: number;
3152
+ aspectRatio: number;
3153
+ };
3154
+
3155
+ export type SanityImageMetadata = {
3156
+ _type: "sanity.imageMetadata";
3157
+ location?: Geopoint;
3158
+ dimensions?: SanityImageDimensions;
3159
+ palette?: SanityImagePalette;
3160
+ lqip?: string;
3161
+ blurHash?: string;
3162
+ thumbHash?: string;
3163
+ hasAlpha?: boolean;
3164
+ isOpaque?: boolean;
3165
+ };
1444
3166
 
1445
- export type SanityPage = {
3167
+ export type SanityFileAsset = {
1446
3168
  _id: string;
3169
+ _type: "sanity.fileAsset";
3170
+ _createdAt: string;
3171
+ _updatedAt: string;
3172
+ _rev: string;
3173
+ originalFilename?: string;
3174
+ label?: string;
1447
3175
  title?: string;
1448
3176
  description?: string;
1449
- slug?: string;
1450
- pageBuilder?: PageBlock[] | null;
1451
- } | null;
1452
- `, ASTRO_PAGE_BUILDER = `---
1453
- import { PortableText } from "astro-portabletext";
1454
- import type { PageBlock } from "../../sanity/types";
1455
- import { urlFor } from "../../sanity/image";
3177
+ altText?: string;
3178
+ sha1hash: string;
3179
+ extension: string;
3180
+ mimeType: string;
3181
+ size: number;
3182
+ assetId: string;
3183
+ uploadId?: string;
3184
+ path: string;
3185
+ url: string;
3186
+ source?: SanityAssetSourceData;
3187
+ };
1456
3188
 
1457
- interface Props {
1458
- blocks: PageBlock[];
1459
- }
3189
+ export type SanityAssetSourceData = {
3190
+ _type: "sanity.assetSourceData";
3191
+ name?: string;
3192
+ id?: string;
3193
+ url?: string;
3194
+ };
1460
3195
 
1461
- const { blocks } = Astro.props;
1462
- ---
3196
+ export type SanityImageAsset = {
3197
+ _id: string;
3198
+ _type: "sanity.imageAsset";
3199
+ _createdAt: string;
3200
+ _updatedAt: string;
3201
+ _rev: string;
3202
+ originalFilename?: string;
3203
+ label?: string;
3204
+ title?: string;
3205
+ description?: string;
3206
+ altText?: string;
3207
+ sha1hash: string;
3208
+ extension: string;
3209
+ mimeType: string;
3210
+ size: number;
3211
+ assetId: string;
3212
+ uploadId?: string;
3213
+ path: string;
3214
+ url: string;
3215
+ metadata?: SanityImageMetadata;
3216
+ source?: SanityAssetSourceData;
3217
+ };
1463
3218
 
1464
- {
1465
- blocks.map((block) => {
1466
- if (block._type === "hero") {
1467
- const src = urlFor(block.image, 1600);
1468
- return (
1469
- <section>
1470
- <h2>{block.heading}</h2>
1471
- {block.subheading && <p>{block.subheading}</p>}
1472
- {src && <img src={src} alt={block.image?.alt ?? ""} />}
1473
- </section>
1474
- );
1475
- }
1476
- if (block._type === "richText") {
1477
- return (
1478
- <section>
1479
- {block.heading && <h2>{block.heading}</h2>}
1480
- {block.content && <PortableText value={block.content} />}
1481
- </section>
1482
- );
1483
- }
1484
- if (block._type === "cta") {
1485
- return (
1486
- <section>
1487
- <h2>{block.heading}</h2>
1488
- {block.text && <p>{block.text}</p>}
1489
- {block.buttonHref && block.buttonLabel && (
1490
- <a href={block.buttonHref}>{block.buttonLabel}</a>
1491
- )}
1492
- </section>
1493
- );
1494
- }
1495
- return null;
1496
- })
1497
- }
1498
- `, ASTRO_SLUG_PAGE = `---
1499
- import { VisualEditing } from "@sanity/astro/visual-editing";
1500
- import PageBuilder from "../components/PageBuilder.astro";
1501
- import { visualEditingEnabled } from "../../sanity/env";
1502
- import { loadQuery } from "../../sanity/fetch";
1503
- import { PAGE_QUERY, PAGE_SLUGS_QUERY } from "../../sanity/queries";
1504
- import type { SanityPage } from "../../sanity/types";
3219
+ export type Geopoint = {
3220
+ _type: "geopoint";
3221
+ lat?: number;
3222
+ lng?: number;
3223
+ alt?: number;
3224
+ };
1505
3225
 
1506
- export async function getStaticPaths() {
1507
- // Returns [] without credentials, so a fresh scaffold builds with no CMS pages.
1508
- const slugs = await loadQuery<(string | null)[]>(PAGE_SLUGS_QUERY);
1509
- return (slugs ?? []).filter(Boolean).map((slug) => ({ params: { slug } }));
1510
- }
3226
+ export type AllSanitySchemaTypes =
3227
+ | SanityImageAssetReference
3228
+ | Image1
3229
+ | Cta
3230
+ | SubscribeNewsletter
3231
+ | FaqReference
3232
+ | FaqAccordion
3233
+ | ImageLinkCards
3234
+ | FeatureCardsIcon
3235
+ | RichTextBlock
3236
+ | Hero
3237
+ | RichText
3238
+ | Button
3239
+ | PageReference
3240
+ | BlogReference
3241
+ | CustomUrl
3242
+ | Settings
3243
+ | SanityImageCrop
3244
+ | SanityImageHotspot
3245
+ | Faq
3246
+ | Author
3247
+ | AuthorReference
3248
+ | Blog
3249
+ | Slug
3250
+ | Page
3251
+ | SanityImagePaletteSwatch
3252
+ | SanityImagePalette
3253
+ | SanityImageDimensions
3254
+ | SanityImageMetadata
3255
+ | SanityFileAsset
3256
+ | SanityAssetSourceData
3257
+ | SanityImageAsset
3258
+ | Geopoint;
3259
+
3260
+ // Source: ../../packages/sanity/src/queries.ts
3261
+ // Variable: HOME_PAGE_QUERY
3262
+ // Query: *[_type == "page" && slug.current == "home"][0]{ _id, _type, title, description, "slug": slug.current, pageBuilder[]{ ..., buttons[]{ _key, _type, text, variant, "openInNewTab": url.openInNewTab, "href": select( url.type == "external" => url.external, url.internal->_type == "blog" => "/blog/" + url.internal->slug.current, url.internal->slug.current == "home" => "/", defined(url.internal->slug.current) => "/" + url.internal->slug.current, "#" ) }, _type == "faqAccordion" => { faqs[]->{ _id, title, richText } }, _type == "imageLinkCards" => { cards[]{ ..., "openInNewTab": url.openInNewTab, "href": select( url.type == "external" => url.external, url.internal->_type == "blog" => "/blog/" + url.internal->slug.current, url.internal->slug.current == "home" => "/", defined(url.internal->slug.current) => "/" + url.internal->slug.current, "#" ) } } }}
3263
+ export type HOME_PAGE_QUERY_RESULT = {
3264
+ _id: string;
3265
+ _type: "page";
3266
+ title: string;
3267
+ description: string | null;
3268
+ slug: string;
3269
+ pageBuilder: Array<
3270
+ | {
3271
+ _key: string;
3272
+ _type: "cta";
3273
+ eyebrow?: string;
3274
+ title?: string;
3275
+ richText?: RichText;
3276
+ buttons: Array<{
3277
+ _key: string;
3278
+ _type: "button";
3279
+ text: string | null;
3280
+ variant: "default" | "link" | "outline" | "secondary" | null;
3281
+ openInNewTab: boolean | null;
3282
+ href: string | "/" | "#" | null;
3283
+ }> | null;
3284
+ }
3285
+ | {
3286
+ _key: string;
3287
+ _type: "faqAccordion";
3288
+ eyebrow?: string;
3289
+ title: string;
3290
+ subtitle?: string;
3291
+ faqs: Array<{
3292
+ _id: string;
3293
+ title: string;
3294
+ richText: RichText | null;
3295
+ }> | null;
3296
+ buttons: null;
3297
+ }
3298
+ | {
3299
+ _key: string;
3300
+ _type: "featureCardsIcon";
3301
+ eyebrow?: string;
3302
+ title?: string;
3303
+ richText?: RichText;
3304
+ cards?: Array<{
3305
+ icon?: string;
3306
+ title?: string;
3307
+ richText?: RichText;
3308
+ _type: "featureCardIcon";
3309
+ _key: string;
3310
+ }>;
3311
+ buttons: null;
3312
+ }
3313
+ | {
3314
+ _key: string;
3315
+ _type: "hero";
3316
+ badge?: string;
3317
+ title?: string;
3318
+ richText?: RichText;
3319
+ image?: Image1;
3320
+ buttons: Array<{
3321
+ _key: string;
3322
+ _type: "button";
3323
+ text: string | null;
3324
+ variant: "default" | "link" | "outline" | "secondary" | null;
3325
+ openInNewTab: boolean | null;
3326
+ href: string | "/" | "#" | null;
3327
+ }> | null;
3328
+ }
3329
+ | {
3330
+ _key: string;
3331
+ _type: "imageLinkCards";
3332
+ eyebrow?: string;
3333
+ title: string;
3334
+ richText?: RichText;
3335
+ buttons: Array<{
3336
+ _key: string;
3337
+ _type: "button";
3338
+ text: string | null;
3339
+ variant: "default" | "link" | "outline" | "secondary" | null;
3340
+ openInNewTab: boolean | null;
3341
+ href: string | "/" | "#" | null;
3342
+ }> | null;
3343
+ cards: Array<{
3344
+ title: string;
3345
+ description?: string;
3346
+ image?: Image1;
3347
+ url?: CustomUrl;
3348
+ _type: "imageLinkCard";
3349
+ _key: string;
3350
+ openInNewTab: boolean | null;
3351
+ href: string | "/" | "#" | null;
3352
+ }> | null;
3353
+ }
3354
+ | {
3355
+ _key: string;
3356
+ _type: "richTextBlock";
3357
+ eyebrow?: string;
3358
+ title?: string;
3359
+ richText?: RichText;
3360
+ buttons: null;
3361
+ }
3362
+ | {
3363
+ _key: string;
3364
+ _type: "subscribeNewsletter";
3365
+ title?: string;
3366
+ subTitle?: RichText;
3367
+ helperText?: RichText;
3368
+ buttons: null;
3369
+ }
3370
+ > | null;
3371
+ } | null;
1511
3372
 
1512
- const page = await loadQuery<SanityPage>(PAGE_QUERY, { slug: Astro.params.slug });
1513
- ---
3373
+ // Source: ../../packages/sanity/src/queries.ts
3374
+ // Variable: PAGE_QUERY
3375
+ // Query: *[_type == "page" && slug.current == $slug][0]{ _id, _type, title, description, "slug": slug.current, pageBuilder[]{ ..., buttons[]{ _key, _type, text, variant, "openInNewTab": url.openInNewTab, "href": select( url.type == "external" => url.external, url.internal->_type == "blog" => "/blog/" + url.internal->slug.current, url.internal->slug.current == "home" => "/", defined(url.internal->slug.current) => "/" + url.internal->slug.current, "#" ) }, _type == "faqAccordion" => { faqs[]->{ _id, title, richText } }, _type == "imageLinkCards" => { cards[]{ ..., "openInNewTab": url.openInNewTab, "href": select( url.type == "external" => url.external, url.internal->_type == "blog" => "/blog/" + url.internal->slug.current, url.internal->slug.current == "home" => "/", defined(url.internal->slug.current) => "/" + url.internal->slug.current, "#" ) } } }}
3376
+ export type PAGE_QUERY_RESULT = {
3377
+ _id: string;
3378
+ _type: "page";
3379
+ title: string;
3380
+ description: string | null;
3381
+ slug: string;
3382
+ pageBuilder: Array<
3383
+ | {
3384
+ _key: string;
3385
+ _type: "cta";
3386
+ eyebrow?: string;
3387
+ title?: string;
3388
+ richText?: RichText;
3389
+ buttons: Array<{
3390
+ _key: string;
3391
+ _type: "button";
3392
+ text: string | null;
3393
+ variant: "default" | "link" | "outline" | "secondary" | null;
3394
+ openInNewTab: boolean | null;
3395
+ href: string | "/" | "#" | null;
3396
+ }> | null;
3397
+ }
3398
+ | {
3399
+ _key: string;
3400
+ _type: "faqAccordion";
3401
+ eyebrow?: string;
3402
+ title: string;
3403
+ subtitle?: string;
3404
+ faqs: Array<{
3405
+ _id: string;
3406
+ title: string;
3407
+ richText: RichText | null;
3408
+ }> | null;
3409
+ buttons: null;
3410
+ }
3411
+ | {
3412
+ _key: string;
3413
+ _type: "featureCardsIcon";
3414
+ eyebrow?: string;
3415
+ title?: string;
3416
+ richText?: RichText;
3417
+ cards?: Array<{
3418
+ icon?: string;
3419
+ title?: string;
3420
+ richText?: RichText;
3421
+ _type: "featureCardIcon";
3422
+ _key: string;
3423
+ }>;
3424
+ buttons: null;
3425
+ }
3426
+ | {
3427
+ _key: string;
3428
+ _type: "hero";
3429
+ badge?: string;
3430
+ title?: string;
3431
+ richText?: RichText;
3432
+ image?: Image1;
3433
+ buttons: Array<{
3434
+ _key: string;
3435
+ _type: "button";
3436
+ text: string | null;
3437
+ variant: "default" | "link" | "outline" | "secondary" | null;
3438
+ openInNewTab: boolean | null;
3439
+ href: string | "/" | "#" | null;
3440
+ }> | null;
3441
+ }
3442
+ | {
3443
+ _key: string;
3444
+ _type: "imageLinkCards";
3445
+ eyebrow?: string;
3446
+ title: string;
3447
+ richText?: RichText;
3448
+ buttons: Array<{
3449
+ _key: string;
3450
+ _type: "button";
3451
+ text: string | null;
3452
+ variant: "default" | "link" | "outline" | "secondary" | null;
3453
+ openInNewTab: boolean | null;
3454
+ href: string | "/" | "#" | null;
3455
+ }> | null;
3456
+ cards: Array<{
3457
+ title: string;
3458
+ description?: string;
3459
+ image?: Image1;
3460
+ url?: CustomUrl;
3461
+ _type: "imageLinkCard";
3462
+ _key: string;
3463
+ openInNewTab: boolean | null;
3464
+ href: string | "/" | "#" | null;
3465
+ }> | null;
3466
+ }
3467
+ | {
3468
+ _key: string;
3469
+ _type: "richTextBlock";
3470
+ eyebrow?: string;
3471
+ title?: string;
3472
+ richText?: RichText;
3473
+ buttons: null;
3474
+ }
3475
+ | {
3476
+ _key: string;
3477
+ _type: "subscribeNewsletter";
3478
+ title?: string;
3479
+ subTitle?: RichText;
3480
+ helperText?: RichText;
3481
+ buttons: null;
3482
+ }
3483
+ > | null;
3484
+ } | null;
1514
3485
 
1515
- <html lang="en">
1516
- <head>
1517
- <meta charset="utf-8" />
1518
- <meta name="viewport" content="width=device-width, initial-scale=1" />
1519
- <title>{page?.title ?? "web"}</title>
1520
- </head>
1521
- <body style="font-family: system-ui, sans-serif; padding: 3rem;">
1522
- <h1>{page?.title}</h1>
1523
- <PageBuilder blocks={page?.pageBuilder ?? []} />
1524
- <VisualEditing enabled={visualEditingEnabled} />
1525
- </body>
1526
- </html>
1527
- `, ASTRO_SANITY_CONFIG, ASTRO_SANITY_CLI = `import { defineCliConfig } from "sanity/cli";
3486
+ // Source: ../../packages/sanity/src/queries.ts
3487
+ // Variable: PAGE_SLUGS_QUERY
3488
+ // Query: *[_type == "page" && defined(slug.current) && slug.current != "home"].slug.current
3489
+ export type PAGE_SLUGS_QUERY_RESULT = Array<string>;
1528
3490
 
1529
- // Run from apps/web. The Sanity CLI is a Node process, so it reads process.env rather
1530
- // than Astro's import.meta.env; astro.config.mjs loads apps/web/.env into it.
1531
- export default defineCliConfig({
1532
- api: {
1533
- projectId: process.env.PUBLIC_SANITY_PROJECT_ID || "placeholder",
1534
- dataset: process.env.PUBLIC_SANITY_DATASET || "production",
1535
- },
1536
- typegen: {
1537
- path: "./sanity/**/*.{ts,tsx}",
1538
- schema: "sanity/schema.json",
1539
- generates: "sanity/sanity.types.ts",
1540
- },
1541
- });
1542
- `, ASTRO_MODULE_REF = '/// <reference types="@sanity/astro/module" />', ASTRO_ENV_LOADER = `// Astro loads .env after this config runs, so pull it in ourselves for the Sanity ids.
1543
- try {
1544
- process.loadEnvFile(".env");
1545
- } catch {
1546
- // No .env — the placeholder ids below keep the build working.
3491
+ // Source: ../../packages/sanity/src/queries.ts
3492
+ // Variable: BLOG_INDEX_QUERY
3493
+ // Query: *[_type == "blog" && defined(slug.current)]|order(publishedAt desc){ _id, title, description, publishedAt, image, "slug": slug.current, "authors": authors[]->{ _id, name, position, image }}
3494
+ export type BLOG_INDEX_QUERY_RESULT = Array<{
3495
+ _id: string;
3496
+ title: string;
3497
+ description: string | null;
3498
+ publishedAt: string | null;
3499
+ image: {
3500
+ asset?: SanityImageAssetReference;
3501
+ media?: unknown;
3502
+ hotspot?: SanityImageHotspot;
3503
+ crop?: SanityImageCrop;
3504
+ alt?: string;
3505
+ _type: "image";
3506
+ } | null;
3507
+ slug: string;
3508
+ authors: Array<{
3509
+ _id: string;
3510
+ name: string;
3511
+ position: string | null;
3512
+ image: {
3513
+ asset?: SanityImageAssetReference;
3514
+ media?: unknown;
3515
+ hotspot?: SanityImageHotspot;
3516
+ crop?: SanityImageCrop;
3517
+ _type: "image";
3518
+ } | null;
3519
+ }> | null;
3520
+ }>;
3521
+
3522
+ // Source: ../../packages/sanity/src/queries.ts
3523
+ // Variable: BLOG_QUERY
3524
+ // Query: *[_type == "blog" && slug.current == $slug][0]{ _id, title, description, publishedAt, image, "slug": slug.current, "authors": authors[]->{ _id, name, position, image }, richText}
3525
+ export type BLOG_QUERY_RESULT = {
3526
+ _id: string;
3527
+ title: string;
3528
+ description: string | null;
3529
+ publishedAt: string | null;
3530
+ image: {
3531
+ asset?: SanityImageAssetReference;
3532
+ media?: unknown;
3533
+ hotspot?: SanityImageHotspot;
3534
+ crop?: SanityImageCrop;
3535
+ alt?: string;
3536
+ _type: "image";
3537
+ } | null;
3538
+ slug: string;
3539
+ authors: Array<{
3540
+ _id: string;
3541
+ name: string;
3542
+ position: string | null;
3543
+ image: {
3544
+ asset?: SanityImageAssetReference;
3545
+ media?: unknown;
3546
+ hotspot?: SanityImageHotspot;
3547
+ crop?: SanityImageCrop;
3548
+ _type: "image";
3549
+ } | null;
3550
+ }> | null;
3551
+ richText: RichText | null;
3552
+ } | null;
3553
+
3554
+ // Source: ../../packages/sanity/src/queries.ts
3555
+ // Variable: BLOG_SLUGS_QUERY
3556
+ // Query: *[_type == "blog" && defined(slug.current)].slug.current
3557
+ export type BLOG_SLUGS_QUERY_RESULT = Array<string>;
3558
+
3559
+ // Source: ../../packages/sanity/src/queries.ts
3560
+ // Variable: SETTINGS_QUERY
3561
+ // Query: *[_type == "settings"][0]{ siteTitle, siteDescription, socialImage }
3562
+ export type SETTINGS_QUERY_RESULT = {
3563
+ siteTitle: string;
3564
+ siteDescription: string | null;
3565
+ socialImage: {
3566
+ asset?: SanityImageAssetReference;
3567
+ media?: unknown;
3568
+ hotspot?: SanityImageHotspot;
3569
+ crop?: SanityImageCrop;
3570
+ _type: "image";
3571
+ } | null;
3572
+ } | null;
3573
+
3574
+ // Query TypeMap
3575
+ import "@sanity/client";
3576
+ declare module "@sanity/client" {
3577
+ interface SanityQueries {
3578
+ '*[_type == "page" && slug.current == "home"][0]{\\n _id,\\n _type,\\n title,\\n description,\\n "slug": slug.current,\\n \\n pageBuilder[]{\\n ...,\\n buttons[]{ _key, _type, text, variant, \\n "openInNewTab": url.openInNewTab,\\n "href": select(\\n url.type == "external" => url.external,\\n url.internal->_type == "blog" => "/blog/" + url.internal->slug.current,\\n url.internal->slug.current == "home" => "/",\\n defined(url.internal->slug.current) => "/" + url.internal->slug.current,\\n "#"\\n )\\n },\\n _type == "faqAccordion" => { faqs[]->{ _id, title, richText } },\\n _type == "imageLinkCards" => { cards[]{ ..., \\n "openInNewTab": url.openInNewTab,\\n "href": select(\\n url.type == "external" => url.external,\\n url.internal->_type == "blog" => "/blog/" + url.internal->slug.current,\\n url.internal->slug.current == "home" => "/",\\n defined(url.internal->slug.current) => "/" + url.internal->slug.current,\\n "#"\\n )\\n } }\\n }\\n\\n}': HOME_PAGE_QUERY_RESULT;
3579
+ '*[_type == "page" && slug.current == $slug][0]{\\n _id,\\n _type,\\n title,\\n description,\\n "slug": slug.current,\\n \\n pageBuilder[]{\\n ...,\\n buttons[]{ _key, _type, text, variant, \\n "openInNewTab": url.openInNewTab,\\n "href": select(\\n url.type == "external" => url.external,\\n url.internal->_type == "blog" => "/blog/" + url.internal->slug.current,\\n url.internal->slug.current == "home" => "/",\\n defined(url.internal->slug.current) => "/" + url.internal->slug.current,\\n "#"\\n )\\n },\\n _type == "faqAccordion" => { faqs[]->{ _id, title, richText } },\\n _type == "imageLinkCards" => { cards[]{ ..., \\n "openInNewTab": url.openInNewTab,\\n "href": select(\\n url.type == "external" => url.external,\\n url.internal->_type == "blog" => "/blog/" + url.internal->slug.current,\\n url.internal->slug.current == "home" => "/",\\n defined(url.internal->slug.current) => "/" + url.internal->slug.current,\\n "#"\\n )\\n } }\\n }\\n\\n}': PAGE_QUERY_RESULT;
3580
+ '*[_type == "page" && defined(slug.current) && slug.current != "home"].slug.current': PAGE_SLUGS_QUERY_RESULT;
3581
+ '*[_type == "blog" && defined(slug.current)]|order(publishedAt desc){\\n _id,\\n title,\\n description,\\n publishedAt,\\n image,\\n "slug": slug.current,\\n "authors": authors[]->{ _id, name, position, image }\\n}': BLOG_INDEX_QUERY_RESULT;
3582
+ '*[_type == "blog" && slug.current == $slug][0]{\\n _id,\\n title,\\n description,\\n publishedAt,\\n image,\\n "slug": slug.current,\\n "authors": authors[]->{ _id, name, position, image }\\n, richText}': BLOG_QUERY_RESULT;
3583
+ '*[_type == "blog" && defined(slug.current)].slug.current': BLOG_SLUGS_QUERY_RESULT;
3584
+ '*[_type == "settings"][0]{ siteTitle, siteDescription, socialImage }': SETTINGS_QUERY_RESULT;
3585
+ }
1547
3586
  }
1548
- `, prependAstroConfigStatement = (content, block, marker) => content.includes(marker) ? content : content.replace("export default defineConfig({", `${block}
3587
+ `;
3588
+
3589
+ // ../../packages/registry/src/content/sanity/index.ts
3590
+ var j6 = (value) => `${JSON.stringify(value, null, 2)}
3591
+ `, SANITY_PACKAGE_JSON, SANITY_BLOCKS_PACKAGE_JSON, prependAstroConfigStatement = (content, block, marker) => content.includes(marker) ? content : content.replace("export default defineConfig({", `${block}
1549
3592
  export default defineConfig({`), addAstroOptimizeDeps = (content) => {
1550
3593
  if (content.includes("react/compiler-runtime"))
1551
3594
  return content;
@@ -1562,23 +3605,12 @@ export default defineConfig({`), addAstroOptimizeDeps = (content) => {
1562
3605
  return content.replace(/vite:\s*\{/, (m) => `${m} ${field}`);
1563
3606
  return content.replace(/defineConfig\(\{/, (m) => `${m}
1564
3607
  vite: { ${field} },`);
1565
- }, addScripts = (pkgJson, scripts) => {
1566
- const pkg = JSON.parse(pkgJson);
1567
- const merged = { ...pkg.scripts, ...scripts };
1568
- const sorted = Object.fromEntries(Object.entries(merged).sort(([a], [b]) => a.localeCompare(b)));
1569
- return `${JSON.stringify({ ...pkg, scripts: sorted }, null, 2)}
1570
- `;
1571
- }, TYPEGEN = "sanity schema extract --enforce-required-fields --path sanity/schema.json && sanity typegen generate", patchWebPackageJson = (content) => addScripts(content, {
1572
- "sanity:typegen": TYPEGEN,
1573
- prebuild: TYPEGEN,
1574
- ...content.includes('"next build"') ? { "precheck-types": TYPEGEN } : {},
1575
- "sanity:seed": "sanity dataset import sanity/seed.ndjson"
1576
- }), NEXT_LAYOUT_SIGNATURE = "export default function RootLayout({ children }: { children: ReactNode }) {", patchNextLayout = (content) => {
3608
+ }, NEXT_LAYOUT_SIGNATURE = "export default function RootLayout({ children }: { children: ReactNode }) {", patchNextLayout = (content) => {
1577
3609
  const withImports = [
1578
3610
  'import { draftMode } from "next/headers";',
1579
3611
  'import { VisualEditing } from "next-sanity/visual-editing";',
1580
3612
  'import { DisableDraftMode } from "../sanity/disable-draft-mode";',
1581
- 'import { SanityLive } from "../sanity/live";'
3613
+ 'import { SanityLive } from "@workspace/sanity/live";'
1582
3614
  ].reduce(ensureImport, content);
1583
3615
  const asAsync = replaceOnce(withImports, NEXT_LAYOUT_SIGNATURE, `export default async function RootLayout({ children }: { children: ReactNode }) {
1584
3616
  const { isEnabled: isDraft } = await draftMode();`);
@@ -1600,15 +3632,15 @@ export default defineConfig({`), addAstroOptimizeDeps = (content) => {
1600
3632
  return content;
1601
3633
  const withImports = [
1602
3634
  'import { VisualEditing } from "@sanity/astro/visual-editing";',
1603
- 'import PageBuilder from "../components/PageBuilder.astro";',
1604
- 'import { visualEditingEnabled } from "../../sanity/env";',
1605
- 'import { loadQuery } from "../../sanity/fetch";',
1606
- 'import { HOME_PAGE_QUERY } from "../../sanity/queries";',
1607
- 'import type { SanityPage } from "../../sanity/types";'
3635
+ 'import { visualEditingEnabled } from "@workspace/sanity/env";',
3636
+ 'import { loadQuery } from "@workspace/sanity/fetch";',
3637
+ 'import { HOME_PAGE_QUERY } from "@workspace/sanity/queries";',
3638
+ 'import type { HOME_PAGE_QUERY_RESULT } from "@workspace/sanity/types";',
3639
+ 'import PageBuilder from "@workspace/sanity-blocks/PageBuilder.astro";'
1608
3640
  ].reduce(ensureImport, content);
1609
3641
  const withFetch = replaceOnce(withImports, 'log.info("rendering home");', `log.info("rendering home");
1610
3642
 
1611
- const page = await loadQuery<SanityPage>(HOME_PAGE_QUERY);`);
3643
+ const page = await loadQuery<HOME_PAGE_QUERY_RESULT>(HOME_PAGE_QUERY);`);
1612
3644
  const withHeading = replaceOnce(withFetch, " <h1>create-turbo-wizard</h1>", ' <h1>{page?.title ?? "create-turbo-wizard"}</h1>');
1613
3645
  const withBlocks = replaceOnce(withHeading, " <p>Astro + Turborepo, scaffolded.</p>", [
1614
3646
  " <PageBuilder blocks={page?.pageBuilder ?? []} />",
@@ -1622,136 +3654,142 @@ const page = await loadQuery<SanityPage>(HOME_PAGE_QUERY);`);
1622
3654
  }, README_SECTION = `
1623
3655
  ## Sanity
1624
3656
 
1625
- The Studio is embedded in \`apps/web\` — Next serves it at \`/studio\`, Astro at \`/admin\`.
1626
- Schema, queries and sample content live in \`apps/web/sanity/\`.
3657
+ The Studio is its own app, \`apps/studio\` (\`pnpm dev:studio\`, port 3333) — a Vite app, so
3658
+ both frontends share the same one. It owns the document schemas, Studio structure and
3659
+ sample content. The read path (client, queries, generated types) is \`packages/sanity\` and
3660
+ the page-builder blocks — schemas and renderers together — are \`packages/sanity-blocks\`.
1627
3661
 
1628
3662
  \`\`\`sh
1629
- cd apps/web
1630
- cp ../../.env.example .env.local # Astro: .env — then fill in the project id
1631
- pnpm sanity:seed # import sanity/seed.ndjson into the configured dataset
1632
- pnpm sanity:typegen # regenerate sanity/sanity.types.ts (also runs on build)
3663
+ cp .env.example apps/web/.env.local # or apps/web/.env on Astro — the project id
3664
+ cp apps/studio/.env.example apps/studio/.env # and SANITY_STUDIO_PROJECT_ID
3665
+ pnpm dev:studio # the Studio on http://localhost:3333
3666
+ cd apps/studio && pnpm sanity:seed # import seed.ndjson into the dataset
3667
+ cd apps/studio && pnpm extract && pnpm type # regenerate packages/sanity/src/sanity.types.ts
1633
3668
  \`\`\`
1634
3669
 
3670
+ \`packages/sanity/src/sanity.types.ts\` ships pre-generated, so a fresh clone type-checks
3671
+ before anything is run; re-run \`extract\` + \`type\` after every schema or query edit.
3672
+
1635
3673
  Without a project id the site builds and serves an empty state — no credentials required.
1636
3674
 
3675
+ ### What's in the schema
3676
+
3677
+ \`page\` documents compose a page builder out of seven blocks — hero, rich text, feature
3678
+ cards, image link cards, FAQ accordion, newsletter and CTA — and \`blog\` posts carry an
3679
+ \`author\` reference, a publish date and a Portable Text body. Pages route by slug
3680
+ (\`/about\`), posts live under \`/blog\`. The shape follows Roboto Studio's
3681
+ \`turbo-start-sanity\`, so components and GROQ from that repo drop in unchanged.
3682
+
3683
+ Links are stored as \`customUrl\` (internal document or external URL) and flattened to a
3684
+ plain \`href\` in \`packages/sanity/src/queries.ts\`, so components never branch on the
3685
+ link type.
3686
+
1637
3687
  ### Visual Editing
1638
3688
 
1639
3689
  Next: open the Studio's **Presentation** tab — draft mode enables via
1640
3690
  \`/api/draft-mode/enable\`; leave it with the on-page "Disable draft mode" button.
1641
3691
  Astro: set \`PUBLIC_SANITY_VISUAL_EDITING_ENABLED=true\` and \`SANITY_API_READ_TOKEN\`,
1642
3692
  then use the dev server — overlays need server rendering (add an SSR adapter for a
1643
- production preview deployment).
3693
+ production preview deployment). \`PUBLIC_SANITY_STUDIO_URL\` is where stega sends a
3694
+ click-through; it defaults to the Studio's own dev origin.
1644
3695
  `, sanity;
1645
3696
  var init_sanity = __esm(() => {
1646
- NEXT_SANITY_CONFIG = `"use client";
1647
-
1648
- import { defineConfig } from "sanity";
1649
- import { presentationTool } from "sanity/presentation";
1650
- import { structureTool } from "sanity/structure";
1651
- import { locations, mainDocuments } from "./sanity/presentation";
1652
- import { schemaTypes } from "./sanity/schema/index";
1653
- import { singletonActions, singletonTypes, structure } from "./sanity/structure";
1654
-
1655
- export default defineConfig({
1656
- name: "default",
1657
- title: "Studio",
1658
- basePath: "/studio",
1659
- projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID || "placeholder",
1660
- dataset: process.env.NEXT_PUBLIC_SANITY_DATASET || "production",
1661
- plugins: [
1662
- presentationTool({
1663
- resolve: { mainDocuments, locations },
1664
- previewUrl: {
1665
- initial: "/",
1666
- previewMode: {
1667
- enable: "/api/draft-mode/enable",
1668
- // Presentation never calls this itself (documented as not yet implemented);
1669
- // editors leave draft mode via <DisableDraftMode/>, which links to the route.
1670
- disable: "/api/draft-mode/disable",
1671
- },
1672
- },
1673
- }),
1674
- structureTool({ structure }),
1675
- ],
1676
- schema: { types: schemaTypes },
1677
- ${DOCUMENT_GUARD}
1678
- });
1679
- `;
1680
- ASTRO_SANITY_CONFIG = `import { defineConfig } from "sanity";
1681
- import { presentationTool } from "sanity/presentation";
1682
- import { structureTool } from "sanity/structure";
1683
- import { locations, mainDocuments } from "./sanity/presentation";
1684
- import { schemaTypes } from "./sanity/schema/index";
1685
- import { singletonActions, singletonTypes, structure } from "./sanity/structure";
1686
-
1687
- export default defineConfig({
1688
- name: "default",
1689
- title: "Studio",
1690
- projectId: import.meta.env.PUBLIC_SANITY_PROJECT_ID || "placeholder",
1691
- dataset: import.meta.env.PUBLIC_SANITY_DATASET || "production",
1692
- plugins: [
1693
- presentationTool({
1694
- resolve: { mainDocuments, locations },
1695
- // No previewMode: Astro has no draft-mode routes. Draft content comes from
1696
- // loadQuery's perspective switch (PUBLIC_SANITY_VISUAL_EDITING_ENABLED + token),
1697
- // which needs a rendering server — \`astro dev\`, or an SSR adapter in production.
1698
- previewUrl: { initial: "/" },
1699
- }),
1700
- structureTool({ structure }),
1701
- ],
1702
- schema: { types: schemaTypes },
1703
- ${DOCUMENT_GUARD}
1704
- });
1705
- `;
3697
+ init_seed();
3698
+ init_studio();
3699
+ SANITY_PACKAGE_JSON = j6({
3700
+ name: "@workspace/sanity",
3701
+ version: "0.0.0",
3702
+ private: true,
3703
+ type: "module",
3704
+ exports: {
3705
+ "./env": "./src/env.ts",
3706
+ "./client": "./src/client.ts",
3707
+ "./live": "./src/live.ts",
3708
+ "./fetch": "./src/fetch.ts",
3709
+ "./image": "./src/image.ts",
3710
+ "./queries": "./src/queries.ts",
3711
+ "./types": "./src/sanity.types.ts"
3712
+ }
3713
+ });
3714
+ SANITY_BLOCKS_PACKAGE_JSON = j6({
3715
+ name: "@workspace/sanity-blocks",
3716
+ version: "0.0.0",
3717
+ private: true,
3718
+ type: "module",
3719
+ exports: {
3720
+ ".": "./src/schema.ts",
3721
+ "./blocks": "./src/blocks.tsx",
3722
+ "./PageBuilder.astro": "./src/PageBuilder.astro"
3723
+ }
3724
+ });
1706
3725
  sanity = {
1707
3726
  id: "sanity",
1708
3727
  kind: "content",
1709
3728
  prompt: {
1710
3729
  label: "Sanity",
1711
- hint: "Headless CMS with an embedded Studio (Next: /studio, Astro: /admin)"
3730
+ hint: "Headless CMS with a Studio of its own (apps/studio)"
1712
3731
  },
1713
3732
  templates: [
1714
- { path: "apps/web/sanity/schema/blocks.ts", content: SCHEMA_BLOCKS },
1715
- { path: "apps/web/sanity/schema/page.ts", content: SCHEMA_PAGE },
1716
- { path: "apps/web/sanity/schema/settings.ts", content: SCHEMA_SETTINGS },
1717
- { path: "apps/web/sanity/schema/index.ts", content: SCHEMA_INDEX },
1718
- { path: "apps/web/sanity/structure.ts", content: STRUCTURE },
1719
- { path: "apps/web/sanity/presentation.ts", content: PRESENTATION },
1720
- { path: "apps/web/sanity/queries.ts", content: QUERIES },
1721
- { path: "apps/web/sanity/seed.ndjson", content: SEED_NDJSON }
3733
+ { path: "packages/sanity/package.json", content: SANITY_PACKAGE_JSON },
3734
+ { path: "packages/sanity/src/queries.ts", content: QUERIES },
3735
+ { path: "packages/sanity/src/sanity.types.ts", content: SANITY_TYPES },
3736
+ { path: "packages/sanity-blocks/package.json", content: SANITY_BLOCKS_PACKAGE_JSON },
3737
+ { path: "packages/sanity-blocks/src/schema.ts", content: SCHEMA_BLOCKS },
3738
+ { path: "apps/studio/package.json", content: STUDIO_PACKAGE_JSON },
3739
+ { path: "apps/studio/tsconfig.json", content: STUDIO_TSCONFIG },
3740
+ { path: "apps/studio/sanity.cli.ts", content: STUDIO_CLI },
3741
+ { path: "apps/studio/schemaTypes/index.ts", content: SCHEMA_INDEX },
3742
+ { path: "apps/studio/schemaTypes/definitions.ts", content: SCHEMA_DEFINITIONS },
3743
+ { path: "apps/studio/schemaTypes/page.ts", content: SCHEMA_PAGE },
3744
+ { path: "apps/studio/schemaTypes/blog.ts", content: SCHEMA_BLOG },
3745
+ { path: "apps/studio/schemaTypes/settings.ts", content: SCHEMA_SETTINGS },
3746
+ { path: "apps/studio/structure.ts", content: STRUCTURE },
3747
+ { path: "apps/studio/presentation.ts", content: PRESENTATION },
3748
+ { path: "apps/studio/seed.ndjson", content: SEED_NDJSON }
1722
3749
  ],
1723
3750
  fragments: {
1724
3751
  next: [
1725
- { path: "apps/web/sanity/env.ts", content: NEXT_ENV },
1726
- { path: "apps/web/sanity/client.ts", content: NEXT_CLIENT },
1727
- { path: "apps/web/sanity/live.ts", content: NEXT_LIVE },
1728
- { path: "apps/web/sanity/fetch.ts", content: NEXT_FETCH },
1729
- { path: "apps/web/sanity/image.ts", content: NEXT_IMAGE },
1730
- { path: "apps/web/sanity/page-builder.tsx", content: NEXT_PAGE_BUILDER },
3752
+ { path: "packages/sanity/src/env.ts", content: NEXT_ENV },
3753
+ { path: "packages/sanity/src/client.ts", content: NEXT_CLIENT },
3754
+ { path: "packages/sanity/src/live.ts", content: NEXT_LIVE },
3755
+ { path: "packages/sanity/src/fetch.ts", content: NEXT_FETCH },
3756
+ { path: "packages/sanity/src/image.ts", content: NEXT_IMAGE },
3757
+ { path: "packages/sanity-blocks/src/blocks.tsx", content: NEXT_PAGE_BUILDER },
3758
+ { path: "apps/studio/sanity.config.ts", content: STUDIO_CONFIG_NEXT },
3759
+ { path: "apps/studio/.env.example", content: STUDIO_ENV_EXAMPLE_NEXT },
1731
3760
  { path: "apps/web/sanity/disable-draft-mode.tsx", content: NEXT_DISABLE_DRAFT_MODE },
1732
- { path: "apps/web/sanity.config.ts", content: NEXT_SANITY_CONFIG },
1733
- { path: "apps/web/sanity.cli.ts", content: NEXT_SANITY_CLI },
1734
- { path: "apps/web/app/studio/[[...tool]]/page.tsx", content: NEXT_STUDIO_PAGE },
1735
3761
  { path: "apps/web/app/api/draft-mode/enable/route.ts", content: NEXT_DRAFT_MODE_ROUTE },
1736
3762
  {
1737
3763
  path: "apps/web/app/api/draft-mode/disable/route.ts",
1738
3764
  content: NEXT_DRAFT_MODE_DISABLE_ROUTE
1739
3765
  },
1740
- { path: "apps/web/app/[slug]/page.tsx", content: NEXT_SLUG_PAGE }
3766
+ { path: "apps/web/app/[slug]/page.tsx", content: NEXT_SLUG_PAGE },
3767
+ { path: "apps/web/app/blog/page.tsx", content: NEXT_BLOG_INDEX_PAGE },
3768
+ { path: "apps/web/app/blog/[slug]/page.tsx", content: NEXT_BLOG_POST_PAGE }
1741
3769
  ],
1742
3770
  astro: [
1743
- { path: "apps/web/sanity/env.ts", content: ASTRO_ENV },
1744
- { path: "apps/web/sanity/fetch.ts", content: ASTRO_FETCH },
1745
- { path: "apps/web/sanity/image.ts", content: ASTRO_IMAGE },
1746
- { path: "apps/web/sanity/types.ts", content: ASTRO_TYPES },
1747
- { path: "apps/web/sanity.config.ts", content: ASTRO_SANITY_CONFIG },
1748
- { path: "apps/web/sanity.cli.ts", content: ASTRO_SANITY_CLI },
1749
- { path: "apps/web/src/components/PageBuilder.astro", content: ASTRO_PAGE_BUILDER },
1750
- { path: "apps/web/src/pages/[slug].astro", content: ASTRO_SLUG_PAGE }
3771
+ { path: "packages/sanity/src/env.ts", content: ASTRO_ENV },
3772
+ { path: "packages/sanity/src/fetch.ts", content: ASTRO_FETCH },
3773
+ { path: "packages/sanity/src/image.ts", content: ASTRO_IMAGE },
3774
+ { path: "packages/sanity-blocks/src/PageBuilder.astro", content: ASTRO_PAGE_BUILDER },
3775
+ { path: "apps/studio/sanity.config.ts", content: STUDIO_CONFIG_ASTRO },
3776
+ { path: "apps/studio/.env.example", content: STUDIO_ENV_EXAMPLE_ASTRO },
3777
+ { path: "apps/web/src/pages/[slug].astro", content: ASTRO_SLUG_PAGE },
3778
+ { path: "apps/web/src/pages/blog/index.astro", content: ASTRO_BLOG_INDEX_PAGE },
3779
+ { path: "apps/web/src/pages/blog/[slug].astro", content: ASTRO_BLOG_POST_PAGE }
1751
3780
  ]
1752
3781
  },
3782
+ scripts: {
3783
+ "dev:studio": "turbo run dev --filter=studio",
3784
+ "build:studio": "turbo run build --filter=studio"
3785
+ },
1753
3786
  deps: [
1754
- { name: "groq", version: "^6.7.0", target: "app" }
3787
+ { name: "@workspace/sanity", version: "workspace:*", target: "app" },
3788
+ { name: "@workspace/sanity-blocks", version: "workspace:*", target: "app" },
3789
+ { name: "groq", version: "^6.7.0", target: "sanity" },
3790
+ { name: "@sanity/client", version: "^7.26.0", target: "sanity" },
3791
+ { name: "sanity", version: "^5.31.1", target: "sanity-blocks" },
3792
+ { name: "@workspace/sanity", version: "workspace:*", target: "sanity-blocks" }
1755
3793
  ],
1756
3794
  frontendDeps: {
1757
3795
  next: [
@@ -1759,59 +3797,40 @@ ${DOCUMENT_GUARD}
1759
3797
  { name: "@portabletext/react", version: "^7.0.1", target: "app" },
1760
3798
  { name: "@sanity/image-url", version: "^2.1.1", target: "app" },
1761
3799
  { name: "@sanity/client", version: "^7.26.0", target: "app" },
1762
- { name: "sanity", version: "^5.31.1", target: "app" },
1763
- { name: "styled-components", version: "^6.1", target: "app" }
3800
+ { name: "next-sanity", version: "^13.2.3", target: "sanity" },
3801
+ { name: "@sanity/image-url", version: "^2.1.1", target: "sanity" },
3802
+ { name: "next", version: "^16.2.0", target: "sanity" },
3803
+ { name: "@portabletext/react", version: "^7.0.1", target: "sanity-blocks" },
3804
+ { name: "next", version: "^16.2.0", target: "sanity-blocks" },
3805
+ { name: "react", version: "^19.0.0", target: "sanity-blocks" }
1764
3806
  ],
1765
3807
  astro: [
1766
3808
  { name: "@sanity/astro", version: "^3.5.0", target: "app" },
1767
3809
  { name: "astro-portabletext", version: "^0.13.0", target: "app" },
1768
- { name: "@portabletext/types", version: "^4.0.2", target: "app" },
1769
3810
  { name: "@sanity/image-url", version: "^2.1.1", target: "app" },
1770
3811
  { name: "@sanity/client", version: "^7.26.0", target: "app" },
1771
- { name: "sanity", version: "^5.31.1", target: "app" },
1772
3812
  { name: "@sanity/ui", version: "^3.5.1", target: "app" },
1773
3813
  { name: "react-compiler-runtime", version: "^1.0.0", target: "app" },
1774
3814
  { name: "react-is", version: "^19.0.0", target: "app" },
1775
3815
  { name: "lodash", version: "^4.17.21", target: "app" },
1776
- { name: "styled-components", version: "^6.1", target: "app" },
1777
- { name: "@types/node", version: "^22.10.0", target: "app", dev: true }
3816
+ { name: "@sanity/image-url", version: "^2.1.1", target: "sanity" },
3817
+ { name: "astro-portabletext", version: "^0.13.0", target: "sanity-blocks" }
1778
3818
  ]
1779
3819
  },
1780
3820
  env: [
1781
3821
  { key: "NEXT_PUBLIC_SANITY_PROJECT_ID", scope: "client" },
1782
3822
  { key: "NEXT_PUBLIC_SANITY_DATASET", scope: "client" },
1783
3823
  { key: "NEXT_PUBLIC_SANITY_API_VERSION", scope: "client" },
3824
+ { key: "NEXT_PUBLIC_SANITY_STUDIO_URL", scope: "client" },
3825
+ { key: "SANITY_STUDIO_PROJECT_ID", scope: "server" },
3826
+ { key: "SANITY_STUDIO_DATASET", scope: "server" },
3827
+ { key: "SANITY_STUDIO_PRESENTATION_URL", scope: "server" },
1784
3828
  { key: "SANITY_API_READ_TOKEN", scope: "server" },
1785
3829
  { key: "PUBLIC_SANITY_PROJECT_ID", scope: "client" },
1786
3830
  { key: "PUBLIC_SANITY_DATASET", scope: "client" },
3831
+ { key: "PUBLIC_SANITY_STUDIO_URL", scope: "client" },
1787
3832
  { key: "PUBLIC_SANITY_VISUAL_EDITING_ENABLED", scope: "client" }
1788
3833
  ],
1789
- owns: [
1790
- "apps/web/sanity/schema/blocks.ts",
1791
- "apps/web/sanity/schema/page.ts",
1792
- "apps/web/sanity/schema/settings.ts",
1793
- "apps/web/sanity/schema/index.ts",
1794
- "apps/web/sanity/structure.ts",
1795
- "apps/web/sanity/presentation.ts",
1796
- "apps/web/sanity/queries.ts",
1797
- "apps/web/sanity/seed.ndjson",
1798
- "apps/web/sanity/env.ts",
1799
- "apps/web/sanity/client.ts",
1800
- "apps/web/sanity/live.ts",
1801
- "apps/web/sanity/fetch.ts",
1802
- "apps/web/sanity/image.ts",
1803
- "apps/web/sanity/types.ts",
1804
- "apps/web/sanity/page-builder.tsx",
1805
- "apps/web/sanity/disable-draft-mode.tsx",
1806
- "apps/web/sanity.config.ts",
1807
- "apps/web/sanity.cli.ts",
1808
- "apps/web/app/studio/[[...tool]]/page.tsx",
1809
- "apps/web/app/api/draft-mode/enable/route.ts",
1810
- "apps/web/app/api/draft-mode/disable/route.ts",
1811
- "apps/web/app/[slug]/page.tsx",
1812
- "apps/web/src/components/PageBuilder.astro",
1813
- "apps/web/src/pages/[slug].astro"
1814
- ],
1815
3834
  patches: [
1816
3835
  {
1817
3836
  path: "apps/web/next.config.ts",
@@ -1821,14 +3840,13 @@ ${DOCUMENT_GUARD}
1821
3840
  { path: "apps/web/app/page.tsx", apply: replaceHomePage(NEXT_HOME_PAGE) },
1822
3841
  {
1823
3842
  path: "apps/web/astro.config.mjs",
1824
- apply: (content) => addAstroOptimizeDeps(addAstroIntegration(prependAstroConfigStatement(content, ASTRO_ENV_LOADER, "process.loadEnvFile"), 'import sanity from "@sanity/astro";', `sanity({ projectId: process.env.PUBLIC_SANITY_PROJECT_ID ?? 'placeholder', dataset: process.env.PUBLIC_SANITY_DATASET ?? 'production', apiVersion: '2026-02-01', useCdn: false, studioBasePath: '/admin', stega: { studioUrl: '/admin' } })`))
3843
+ apply: (content) => addAstroOptimizeDeps(addAstroIntegration(prependAstroConfigStatement(content, ASTRO_ENV_LOADER, "process.loadEnvFile"), 'import sanity from "@sanity/astro";', `sanity({ projectId: process.env.PUBLIC_SANITY_PROJECT_ID ?? 'placeholder', dataset: process.env.PUBLIC_SANITY_DATASET ?? 'production', apiVersion: '2026-02-01', useCdn: false, stega: { studioUrl: process.env.PUBLIC_SANITY_STUDIO_URL ?? 'http://localhost:3333' } })`))
1825
3844
  },
1826
3845
  {
1827
3846
  path: "apps/web/src/env.d.ts",
1828
3847
  apply: (content) => ensureImport(content, ASTRO_MODULE_REF)
1829
3848
  },
1830
3849
  { path: "apps/web/src/pages/index.astro", apply: patchAstroIndex },
1831
- { path: "apps/web/package.json", apply: patchWebPackageJson },
1832
3850
  {
1833
3851
  path: "README.md",
1834
3852
  apply: (content) => content.includes("## Sanity") ? content : content + README_SECTION
@@ -1838,7 +3856,7 @@ ${DOCUMENT_GUARD}
1838
3856
  });
1839
3857
 
1840
3858
  // ../../packages/registry/src/frontends/astro.ts
1841
- var j4 = (value) => `${JSON.stringify(value, null, 2)}
3859
+ var j7 = (value) => `${JSON.stringify(value, null, 2)}
1842
3860
  `, ASTRO_CONFIG = `import react from "@astrojs/react";
1843
3861
  import { defineConfig } from "astro/config";
1844
3862
 
@@ -1871,7 +3889,7 @@ var init_astro = __esm(() => {
1871
3889
  templates: [
1872
3890
  {
1873
3891
  path: "apps/web/package.json",
1874
- content: j4({
3892
+ content: j7({
1875
3893
  name: "web",
1876
3894
  version: "0.0.0",
1877
3895
  private: true,
@@ -1903,7 +3921,7 @@ var init_astro = __esm(() => {
1903
3921
  { path: "apps/web/astro.config.mjs", content: ASTRO_CONFIG },
1904
3922
  {
1905
3923
  path: "apps/web/tsconfig.json",
1906
- content: j4({
3924
+ content: j7({
1907
3925
  extends: "astro/tsconfigs/strict",
1908
3926
  compilerOptions: { jsx: "react-jsx", jsxImportSource: "react" },
1909
3927
  include: [".astro/types.d.ts", "**/*"],
@@ -1913,17 +3931,44 @@ var init_astro = __esm(() => {
1913
3931
  { path: "apps/web/src/env.d.ts", content: `/// <reference types="astro/client" />
1914
3932
  ` },
1915
3933
  { path: "apps/web/src/pages/index.astro", content: INDEX_ASTRO }
1916
- ],
1917
- owns: [
1918
- "apps/web/package.json",
1919
- "apps/web/astro.config.mjs",
1920
- "apps/web/tsconfig.json",
1921
- "apps/web/src/env.d.ts",
1922
- "apps/web/src/pages/index.astro"
1923
3934
  ]
1924
3935
  };
1925
3936
  });
1926
3937
 
3938
+ // ../../packages/registry/src/frontends/deploy-target.ts
3939
+ var ASTRO_SSR_MODULES, ASTROJS_VERCEL_VERSION = "^9.0.5", NODE_IMPORT = 'import node from "@astrojs/node";', NODE_ADAPTER = 'node({ mode: "standalone" })', VERCEL_IMPORT = 'import vercel from "@astrojs/vercel";', VERCEL_ADAPTER = "vercel()", deployDescriptor = (config) => {
3940
+ if (config.frontend !== "astro" || config.deploy !== "vercel")
3941
+ return null;
3942
+ if (!config.modules.some((id) => ASTRO_SSR_MODULES.includes(id)))
3943
+ return null;
3944
+ return {
3945
+ id: "deploy-vercel",
3946
+ kind: "frontend",
3947
+ frontendDeps: {
3948
+ astro: [{ name: "@astrojs/vercel", version: ASTROJS_VERCEL_VERSION, target: "app" }]
3949
+ },
3950
+ patches: [
3951
+ {
3952
+ path: "apps/web/astro.config.mjs",
3953
+ apply: (content) => {
3954
+ if (content.includes(VERCEL_IMPORT))
3955
+ return content;
3956
+ if (!content.includes(NODE_IMPORT))
3957
+ return content;
3958
+ return content.replace(NODE_IMPORT, VERCEL_IMPORT).replace(NODE_ADAPTER, VERCEL_ADAPTER);
3959
+ }
3960
+ },
3961
+ {
3962
+ path: "apps/web/package.json",
3963
+ apply: (content) => content.includes('"@astrojs/node"') ? content.replace(/^\s*"@astrojs\/node":\s*"[^"]*",?\n/m, "") : content
3964
+ }
3965
+ ]
3966
+ };
3967
+ };
3968
+ var init_deploy_target = __esm(() => {
3969
+ ASTRO_SSR_MODULES = ["forms"];
3970
+ });
3971
+
1927
3972
  // ../../packages/registry/src/frontends/experimental.ts
1928
3973
  var BABEL_REACT_COMPILER_VERSION = "^1.0.0", experimentalDescriptor = (config) => {
1929
3974
  const { cacheComponents, reactCompiler } = config.experimental;
@@ -1960,7 +4005,7 @@ var BABEL_REACT_COMPILER_VERSION = "^1.0.0", experimentalDescriptor = (config) =
1960
4005
  var init_experimental = () => {};
1961
4006
 
1962
4007
  // ../../packages/registry/src/frontends/next.ts
1963
- var j5 = (value) => `${JSON.stringify(value, null, 2)}
4008
+ var j8 = (value) => `${JSON.stringify(value, null, 2)}
1964
4009
  `, NEXT_CONFIG = `import { createRequire } from "node:module";
1965
4010
  import type { NextConfig } from "next";
1966
4011
 
@@ -2019,7 +4064,7 @@ var init_next = __esm(() => {
2019
4064
  templates: [
2020
4065
  {
2021
4066
  path: "apps/web/package.json",
2022
- content: j5({
4067
+ content: j8({
2023
4068
  name: "web",
2024
4069
  version: "0.0.0",
2025
4070
  private: true,
@@ -2050,7 +4095,7 @@ var init_next = __esm(() => {
2050
4095
  { path: "apps/web/next.config.ts", content: NEXT_CONFIG },
2051
4096
  {
2052
4097
  path: "apps/web/tsconfig.json",
2053
- content: j5({
4098
+ content: j8({
2054
4099
  extends: "@workspace/typescript-config/nextjs.json",
2055
4100
  compilerOptions: { plugins: [{ name: "next" }], paths: { "@/*": ["./*"] } },
2056
4101
  include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
@@ -2060,14 +4105,6 @@ var init_next = __esm(() => {
2060
4105
  { path: "apps/web/app/layout.tsx", content: LAYOUT },
2061
4106
  { path: "apps/web/app/page.tsx", content: PAGE },
2062
4107
  { path: "apps/web/app/globals.css", content: GLOBALS }
2063
- ],
2064
- owns: [
2065
- "apps/web/package.json",
2066
- "apps/web/next.config.ts",
2067
- "apps/web/tsconfig.json",
2068
- "apps/web/app/layout.tsx",
2069
- "apps/web/app/page.tsx",
2070
- "apps/web/app/globals.css"
2071
4108
  ]
2072
4109
  };
2073
4110
  });
@@ -2236,18 +4273,12 @@ export default function AlgoliaSearch({
2236
4273
  { key: "PUBLIC_ALGOLIA_APP_ID", scope: "client" },
2237
4274
  { key: "PUBLIC_ALGOLIA_SEARCH_KEY", scope: "client" },
2238
4275
  { key: "PUBLIC_ALGOLIA_INDEX", scope: "client" }
2239
- ],
2240
- owns: [
2241
- "apps/web/app/search/Search.tsx",
2242
- "apps/web/app/search/page.tsx",
2243
- "apps/web/src/components/AlgoliaSearch.tsx",
2244
- "apps/web/src/pages/search.astro"
2245
4276
  ]
2246
4277
  };
2247
4278
  });
2248
4279
 
2249
4280
  // ../../packages/registry/src/modules/changesets.ts
2250
- var j6 = (v) => `${JSON.stringify(v, null, 2)}
4281
+ var j9 = (v) => `${JSON.stringify(v, null, 2)}
2251
4282
  `, CONFIG, README = `# Changesets
2252
4283
 
2253
4284
  This folder is managed by [Changesets](https://github.com/changesets/changesets).
@@ -2258,7 +4289,7 @@ the \`version-packages\` script consumes them to update versions and changelogs,
2258
4289
  the \`release\` script builds and publishes.
2259
4290
  `, changesets;
2260
4291
  var init_changesets = __esm(() => {
2261
- CONFIG = j6({
4292
+ CONFIG = j9({
2262
4293
  $schema: "https://unpkg.com/@changesets/config@3.1.4/schema.json",
2263
4294
  changelog: "@changesets/cli/changelog",
2264
4295
  commit: false,
@@ -2286,8 +4317,7 @@ var init_changesets = __esm(() => {
2286
4317
  changeset: "changeset",
2287
4318
  "version-packages": "changeset version",
2288
4319
  release: "turbo run build && changeset publish"
2289
- },
2290
- owns: [".changeset/config.json", ".changeset/README.md"]
4320
+ }
2291
4321
  };
2292
4322
  });
2293
4323
 
@@ -2336,8 +4366,7 @@ var init_e2e = __esm(() => {
2336
4366
  ]
2337
4367
  },
2338
4368
  scripts: { e2e: "playwright test" },
2339
- deps: [{ name: "@playwright/test", version: "^1.60.0", target: "root", dev: true }],
2340
- owns: ["tests/e2e/home.spec.ts", "playwright.config.ts"]
4369
+ deps: [{ name: "@playwright/test", version: "^1.60.0", target: "root", dev: true }]
2341
4370
  };
2342
4371
  });
2343
4372
 
@@ -2577,13 +4606,6 @@ export const POST: APIRoute = async ({ request }) => {
2577
4606
  path: "apps/web/astro.config.mjs",
2578
4607
  apply: (content) => ensureAstroAdapter(content, 'import node from "@astrojs/node";', 'node({ mode: "standalone" })')
2579
4608
  }
2580
- ],
2581
- owns: [
2582
- "apps/web/app/contact/page.tsx",
2583
- "apps/web/app/api/contact/route.ts",
2584
- "apps/web/src/components/ContactForm.tsx",
2585
- "apps/web/src/pages/contact.astro",
2586
- "apps/web/src/pages/api/contact.ts"
2587
4609
  ]
2588
4610
  };
2589
4611
  });
@@ -2625,8 +4647,7 @@ var init_git_hooks = __esm(() => {
2625
4647
  target: "root",
2626
4648
  dev: true
2627
4649
  }
2628
- ],
2629
- owns: ["lefthook.yml", "commitlint.config.mjs"]
4650
+ ]
2630
4651
  };
2631
4652
  });
2632
4653
 
@@ -2680,8 +4701,7 @@ var init_github_ci = __esm(() => {
2680
4701
  label: "GitHub Actions CI",
2681
4702
  hint: "Workflow that installs, type-checks, and builds on every push and PR."
2682
4703
  },
2683
- templates: [{ path: ".github/workflows/ci.yml", content: CI_WORKFLOW }],
2684
- owns: [".github/workflows/ci.yml"]
4704
+ templates: [{ path: ".github/workflows/ci.yml", content: CI_WORKFLOW }]
2685
4705
  };
2686
4706
  });
2687
4707
 
@@ -2844,18 +4864,7 @@ ${LLMS_TXT_REST}\`;
2844
4864
  { path: "apps/web/src/content/pages.ts", content: ASTRO_CONTENT },
2845
4865
  { path: "apps/web/src/pages/[slug].md.ts", content: ASTRO_MD_ENDPOINT }
2846
4866
  ]
2847
- },
2848
- owns: [
2849
- "skills.md",
2850
- "apps/web/app/llms.txt/route.ts",
2851
- "apps/web/app/docs/content.ts",
2852
- "apps/web/app/docs/page.tsx",
2853
- "apps/web/app/docs.md/route.ts",
2854
- "apps/web/app/lib/markdown-variant.ts",
2855
- "apps/web/src/pages/llms.txt.ts",
2856
- "apps/web/src/content/pages.ts",
2857
- "apps/web/src/pages/[slug].md.ts"
2858
- ]
4867
+ }
2859
4868
  };
2860
4869
  });
2861
4870
 
@@ -2916,7 +4925,6 @@ var init_posthog = __esm(() => {
2916
4925
  { key: "PUBLIC_POSTHOG_KEY", scope: "client" },
2917
4926
  { key: "PUBLIC_POSTHOG_HOST", scope: "client" }
2918
4927
  ],
2919
- owns: ["apps/web/instrumentation-client.ts", "apps/web/src/components/PostHog.astro"],
2920
4928
  patches: [
2921
4929
  { path: "apps/web/src/pages/index.astro", apply: patchIndexAstro },
2922
4930
  { path: "apps/web/src/layouts/Layout.astro", apply: patchIndexAstro }
@@ -3444,15 +5452,6 @@ var init_sections = __esm(() => {
3444
5452
  { path: "packages/ui/src/components/sections/faq.tsx", content: FAQ_TSX },
3445
5453
  { path: "packages/ui/src/components/sections/cta.tsx", content: CTA_TSX }
3446
5454
  ],
3447
- owns: [
3448
- "packages/ui/src/components/sections/hero.tsx",
3449
- "packages/ui/src/components/sections/features.tsx",
3450
- "packages/ui/src/components/sections/logo-cloud.tsx",
3451
- "packages/ui/src/components/sections/testimonials.tsx",
3452
- "packages/ui/src/components/sections/pricing.tsx",
3453
- "packages/ui/src/components/sections/faq.tsx",
3454
- "packages/ui/src/components/sections/cta.tsx"
3455
- ],
3456
5455
  patches: [
3457
5456
  { path: "apps/web/app/page.tsx", apply: replaceWith(COMPOSED_NEXT_HOME_PAGE) },
3458
5457
  { path: "apps/web/src/pages/index.astro", apply: replaceWith(COMPOSED_ASTRO_HOME_PAGE) }
@@ -3720,14 +5719,6 @@ var init_seo = __esm(() => {
3720
5719
  { key: "PUBLIC_SITE_NAME", scope: "client" },
3721
5720
  { key: "PUBLIC_SITE_DESCRIPTION", scope: "client" }
3722
5721
  ],
3723
- owns: [
3724
- "apps/web/app/lib/metadata.ts",
3725
- "apps/web/app/sitemap.ts",
3726
- "apps/web/app/robots.ts",
3727
- "apps/web/app/opengraph-image.tsx",
3728
- "apps/web/src/components/SEO.astro",
3729
- "apps/web/src/pages/og.png.ts"
3730
- ],
3731
5722
  patches: [
3732
5723
  { path: "apps/web/astro.config.mjs", apply: patchAstroConfig },
3733
5724
  { path: "apps/web/app/layout.tsx", apply: patchLayout },
@@ -4048,7 +6039,7 @@ export function SiteHeader({
4048
6039
  ))}
4049
6040
  </nav>
4050
6041
  <div className="ml-auto">
4051
- <Button size="sm" render={<a href={action.href} />}>
6042
+ <Button size="sm" nativeButton={false} render={<a href={action.href} />}>
4052
6043
  {action.label}
4053
6044
  </Button>
4054
6045
  </div>
@@ -4111,7 +6102,9 @@ export default function NotFound() {
4111
6102
  <p className="text-muted-foreground">
4112
6103
  The link may be out of date, or the page may have moved.
4113
6104
  </p>
4114
- <Button render={<a href="/" />}>Back home</Button>
6105
+ <Button nativeButton={false} render={<a href="/" />}>
6106
+ Back home
6107
+ </Button>
4115
6108
  </main>
4116
6109
  );
4117
6110
  }
@@ -4562,8 +6555,8 @@ ${DARK_TOKENS}
4562
6555
  });
4563
6556
 
4564
6557
  // ../../packages/registry/src/modules/ui/index.ts
4565
- var j7 = (value) => `${JSON.stringify(value, null, 2)}
4566
- `, UI_PACKAGE_JSON, componentsJson = (rsc) => j7({
6558
+ var j10 = (value) => `${JSON.stringify(value, null, 2)}
6559
+ `, UI_PACKAGE_JSON, componentsJson = (rsc) => j10({
4567
6560
  $schema: "https://ui.shadcn.com/schema.json",
4568
6561
  style: "base-nova",
4569
6562
  rsc,
@@ -4586,20 +6579,22 @@ var j7 = (value) => `${JSON.stringify(value, null, 2)}
4586
6579
  menuColor: "default",
4587
6580
  menuAccent: "subtle"
4588
6581
  }), patchNextLayout2 = (content) => {
4589
- if (content.includes("@workspace/ui/components/site-header"))
6582
+ if (content.includes("<SiteHeader"))
4590
6583
  return content;
4591
6584
  const withImports = [
4592
6585
  'import { SiteFooter } from "@workspace/ui/components/site-footer";',
4593
6586
  'import { SiteHeader } from "@workspace/ui/components/site-header";',
4594
- 'import rootPkg from "../../../package.json";',
4595
- content
6587
+ 'import rootPkg from "../../../package.json";'
6588
+ ].reduce(ensureImport, content);
6589
+ const expanded = withImports.replace(/^([ \t]*)<body([^>]*)>\{children\}<\/body>/m, (_m, indent, attrs) => `${indent}<body${attrs}>
6590
+ ${indent} {children}
6591
+ ${indent}</body>`);
6592
+ return expanded.replace(/<body([^>]*)>/, '<body$1 className="flex min-h-svh flex-col">').replace(/^([ \t]*)\{children\}$/m, (_m, indent) => [
6593
+ `${indent}<SiteHeader siteName={rootPkg.name} />`,
6594
+ `${indent}<div className="flex-1">{children}</div>`,
6595
+ `${indent}<SiteFooter siteName={rootPkg.name} />`
4596
6596
  ].join(`
4597
- `);
4598
- return withImports.replace(/<body([^>]*)>\{children\}<\/body>/, `<body$1 className="flex min-h-svh flex-col">
4599
- <SiteHeader siteName={rootPkg.name} />
4600
- <div className="flex-1">{children}</div>
4601
- <SiteFooter siteName={rootPkg.name} />
4602
- </body>`);
6597
+ `));
4603
6598
  }, NEXT_POSTCSS_CONFIG = `export default {
4604
6599
  plugins: {
4605
6600
  "@tailwindcss/postcss": {},
@@ -4610,7 +6605,7 @@ var j7 = (value) => `${JSON.stringify(value, null, 2)}
4610
6605
  var init_ui = __esm(() => {
4611
6606
  init_pages();
4612
6607
  init_theme();
4613
- UI_PACKAGE_JSON = j7({
6608
+ UI_PACKAGE_JSON = j10({
4614
6609
  name: "@workspace/ui",
4615
6610
  version: "0.0.0",
4616
6611
  private: true,
@@ -4674,25 +6669,6 @@ var init_ui = __esm(() => {
4674
6669
  ]
4675
6670
  },
4676
6671
  deps: [{ name: "@workspace/ui", version: "workspace:*", target: "app" }],
4677
- owns: [
4678
- "packages/ui/package.json",
4679
- "packages/ui/components.json",
4680
- "packages/ui/src/styles/globals.css",
4681
- "packages/ui/src/lib/utils.ts",
4682
- "packages/ui/src/components/button.tsx",
4683
- "packages/ui/src/components/card.tsx",
4684
- "packages/ui/src/components/input.tsx",
4685
- "packages/ui/src/components/textarea.tsx",
4686
- "packages/ui/src/components/label.tsx",
4687
- "packages/ui/src/components/badge.tsx",
4688
- "packages/ui/src/components/site-header.tsx",
4689
- "packages/ui/src/components/site-footer.tsx",
4690
- "apps/web/postcss.config.mjs",
4691
- "apps/web/components.json",
4692
- "apps/web/app/not-found.tsx",
4693
- "apps/web/src/layouts/Layout.astro",
4694
- "apps/web/src/pages/404.astro"
4695
- ],
4696
6672
  patches: [
4697
6673
  {
4698
6674
  path: "apps/web/app/globals.css",
@@ -4710,7 +6686,7 @@ var init_ui = __esm(() => {
4710
6686
  if (!plugin || pkg.devDependencies?.[plugin])
4711
6687
  return content;
4712
6688
  const devDependencies = Object.fromEntries(Object.entries({ ...pkg.devDependencies, [plugin]: "^4.3.0" }).sort(([a], [b]) => a.localeCompare(b)));
4713
- return j7({ ...pkg, devDependencies });
6689
+ return j10({ ...pkg, devDependencies });
4714
6690
  }
4715
6691
  },
4716
6692
  { path: "apps/web/app/layout.tsx", apply: patchNextLayout2 },
@@ -4751,8 +6727,7 @@ var init_vitest = __esm(() => {
4751
6727
  { path: "tests/example.test.ts", content: EXAMPLE_TEST }
4752
6728
  ],
4753
6729
  scripts: { test: "vitest run", "test:watch": "vitest" },
4754
- deps: [{ name: "vitest", version: "^4.1.0", target: "root", dev: true }],
4755
- owns: ["vitest.config.ts", "tests/example.test.ts"]
6730
+ deps: [{ name: "vitest", version: "^4.1.0", target: "root", dev: true }]
4756
6731
  };
4757
6732
  });
4758
6733
 
@@ -4760,12 +6735,14 @@ var init_vitest = __esm(() => {
4760
6735
  function buildRegistry(config2) {
4761
6736
  const content = CONTENT_DESCRIPTORS[config2.content];
4762
6737
  const experimental = experimentalDescriptor(config2);
6738
+ const deploy = deployDescriptor(config2);
4763
6739
  return [
4764
6740
  ...baselineDescriptors,
4765
6741
  FRONTEND_DESCRIPTORS[config2.frontend],
4766
6742
  ...experimental ? [experimental] : [],
4767
6743
  ...content ? [content] : [],
4768
- ...config2.modules.map((id) => MODULE_DESCRIPTORS[id])
6744
+ ...config2.modules.map((id) => MODULE_DESCRIPTORS[id]),
6745
+ ...deploy ? [deploy] : []
4769
6746
  ];
4770
6747
  }
4771
6748
  var baselineDescriptors, FRONTEND_DESCRIPTORS, MODULE_DESCRIPTORS, CONTENT_DESCRIPTORS;
@@ -4777,6 +6754,7 @@ var init_src3 = __esm(() => {
4777
6754
  init_markdown();
4778
6755
  init_sanity();
4779
6756
  init_astro();
6757
+ init_deploy_target();
4780
6758
  init_experimental();
4781
6759
  init_next();
4782
6760
  init_algolia();
@@ -5003,6 +6981,7 @@ var CreateInput = z2.object({
5003
6981
  frontend: z2.enum(FRONTENDS).optional(),
5004
6982
  content: z2.enum(CONTENT_SOURCES).optional(),
5005
6983
  packageManager: z2.enum(PACKAGE_MANAGERS).optional(),
6984
+ deploy: z2.enum(DEPLOY_TARGETS).optional(),
5006
6985
  modules: z2.array(z2.enum(MODULES)).optional(),
5007
6986
  preset: z2.enum(PRESETS).optional(),
5008
6987
  cacheComponents: z2.boolean().optional(),
@@ -5020,6 +6999,7 @@ var router = t.router({
5020
6999
  frontend: input.frontend,
5021
7000
  content: input.content ?? base.content,
5022
7001
  packageManager: input.packageManager ?? base.packageManager,
7002
+ deploy: input.deploy ?? base.deploy,
5023
7003
  modules: input.modules ?? base.modules,
5024
7004
  cacheComponents: input.cacheComponents ?? base.experimental?.cacheComponents,
5025
7005
  reactCompiler: input.reactCompiler ?? base.experimental?.reactCompiler
@@ -5030,6 +7010,7 @@ var router = t.router({
5030
7010
  frontend: flags.frontend ?? "next",
5031
7011
  content: flags.content,
5032
7012
  packageManager: flags.packageManager,
7013
+ deploy: flags.deploy,
5033
7014
  modules: flags.modules,
5034
7015
  experimental: {
5035
7016
  cacheComponents: flags.cacheComponents,