create-turbo-wizard 0.1.0 → 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 +2761 -779
  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
  });
@@ -67,6 +70,72 @@ var init_src = __esm(() => {
67
70
  };
68
71
  });
69
72
 
73
+ // package.json
74
+ var package_default;
75
+ var init_package = __esm(() => {
76
+ package_default = {
77
+ name: "create-turbo-wizard",
78
+ version: "0.1.2",
79
+ description: "Scaffold a Turborepo monorepo: Astro or Next.js, Sanity or Markdown, opt-in modules.",
80
+ keywords: [
81
+ "turborepo",
82
+ "monorepo",
83
+ "scaffold",
84
+ "astro",
85
+ "nextjs",
86
+ "sanity",
87
+ "shadcn",
88
+ "tailwind",
89
+ "marketing-site",
90
+ "create"
91
+ ],
92
+ homepage: "https://turbo-wizard.vercel.app",
93
+ repository: {
94
+ type: "git",
95
+ url: "git+https://github.com/robotostudio/turbo-wizard.git",
96
+ directory: "apps/cli"
97
+ },
98
+ bugs: {
99
+ url: "https://github.com/robotostudio/turbo-wizard/issues"
100
+ },
101
+ author: "Roboto Studio",
102
+ license: "MIT",
103
+ type: "module",
104
+ bin: {
105
+ "create-turbo-wizard": "dist/cli.js"
106
+ },
107
+ files: [
108
+ "dist",
109
+ "README.md"
110
+ ],
111
+ scripts: {
112
+ "check-types": "tsc --noEmit",
113
+ test: "bun test",
114
+ start: "bun run src/cli.ts",
115
+ build: "bun run scripts/bundle.ts",
116
+ prepublishOnly: "bun run scripts/bundle.ts"
117
+ },
118
+ dependencies: {
119
+ "@clack/prompts": "^1.5.0",
120
+ "@modelcontextprotocol/sdk": "^1.29.0",
121
+ "@trpc/server": "^11.0.0",
122
+ "trpc-cli": "^0.14.1",
123
+ zod: "^4.4.3"
124
+ },
125
+ devDependencies: {
126
+ "@turbo-wizard/engine": "workspace:*",
127
+ "@turbo-wizard/registry": "workspace:*",
128
+ "@turbo-wizard/types": "workspace:*"
129
+ },
130
+ engines: {
131
+ node: ">=22"
132
+ },
133
+ publishConfig: {
134
+ access: "public"
135
+ }
136
+ };
137
+ });
138
+
70
139
  // ../../packages/engine/src/result.ts
71
140
  var ok = (value) => ({ ok: true, value }), err = (error) => ({ ok: false, error }), CompatibilityError, CollisionError;
72
141
  var init_result = __esm(() => {
@@ -223,6 +292,7 @@ ${WORKSPACE_GLOBS.map((g) => ` - "${g}"`).join(`
223
292
  "dist/",
224
293
  ".next/",
225
294
  ".astro/",
295
+ ".sanity/",
226
296
  ".turbo/",
227
297
  "*.tsbuildinfo",
228
298
  "",
@@ -314,8 +384,7 @@ var init_biome = __esm(() => {
314
384
  kind: "baseline",
315
385
  templates: [{ path: "biome.jsonc", content: BIOME_CONFIG }],
316
386
  scripts: { check: "biome check .", format: "biome format --write ." },
317
- deps: [{ name: "@biomejs/biome", versionKey: "@biomejs/biome", target: "root", dev: true }],
318
- owns: ["biome.jsonc"]
387
+ deps: [{ name: "@biomejs/biome", versionKey: "@biomejs/biome", target: "root", dev: true }]
319
388
  };
320
389
  });
321
390
 
@@ -360,8 +429,7 @@ var init_env = __esm(() => {
360
429
  content: j({ extends: "@workspace/typescript-config/base.json", include: ["src"] })
361
430
  },
362
431
  { path: "packages/env/src/index.ts", content: ENV_SRC }
363
- ],
364
- owns: ["packages/env/package.json", "packages/env/tsconfig.json", "packages/env/src/index.ts"]
432
+ ]
365
433
  };
366
434
  });
367
435
 
@@ -409,11 +477,6 @@ var init_logger = __esm(() => {
409
477
  content: j2({ extends: "@workspace/typescript-config/base.json", include: ["src"] })
410
478
  },
411
479
  { path: "packages/logger/src/index.ts", content: LOGGER_SRC }
412
- ],
413
- owns: [
414
- "packages/logger/package.json",
415
- "packages/logger/tsconfig.json",
416
- "packages/logger/src/index.ts"
417
480
  ]
418
481
  };
419
482
  });
@@ -477,12 +540,6 @@ var init_typescript_config = __esm(() => {
477
540
  compilerOptions: { lib: ["DOM", "DOM.Iterable", "ES2023"], jsx: "react-jsx" }
478
541
  })
479
542
  }
480
- ],
481
- owns: [
482
- "packages/typescript-config/package.json",
483
- "packages/typescript-config/base.json",
484
- "packages/typescript-config/nextjs.json",
485
- "packages/typescript-config/react-library.json"
486
543
  ]
487
544
  };
488
545
  });
@@ -550,7 +607,8 @@ function replaceNextMetadata(content, expr) {
550
607
  }
551
608
 
552
609
  // ../../packages/registry/src/content/markdown.ts
553
- 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";
554
612
 
555
613
  const components: MDXComponents = {
556
614
  // Override built-ins here, e.g. h1: ({ children }) => <h1 className="...">{children}</h1>,
@@ -576,15 +634,26 @@ This post is written in **MDX** — Markdown with JSX.
576
634
  - Mix Markdown prose with interactive components
577
635
  - Full JSX expression support
578
636
  - Server Components by default
637
+ `, NEXT_CONTENT_SCHEMA = `export interface PostMetadata {
638
+ title: string;
639
+ description?: string;
640
+ date?: string;
641
+ }
579
642
  `, NEXT_BLOG_POSTS_TS = `import { readdir } from "node:fs/promises";
580
643
  import path from "node:path";
644
+ import type { PostMetadata } from "@workspace/content/schema";
581
645
  import type { ComponentType } from "react";
582
646
 
583
- 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");
584
653
 
585
654
  export interface PostModule {
586
655
  default: ComponentType;
587
- metadata: { title: string; description?: string; date?: string };
656
+ metadata: PostMetadata;
588
657
  }
589
658
 
590
659
  export async function getPostSlugs(): Promise<string[]> {
@@ -599,7 +668,7 @@ async function getPosts() {
599
668
  const slugs = await getPostSlugs();
600
669
  return Promise.all(
601
670
  slugs.map(async (slug) => {
602
- const { metadata } = (await import(\`@/content/blog/\${slug}.mdx\`)) as PostModule;
671
+ const { metadata } = (await import(\`@workspace/content/blog/\${slug}.mdx\`)) as PostModule;
603
672
  return { slug, ...metadata };
604
673
  }),
605
674
  );
@@ -653,7 +722,7 @@ export default async function BlogPostPage({
653
722
  params: Promise<{ slug: string }>;
654
723
  }) {
655
724
  const { slug } = await params;
656
- 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;
657
726
  if (!post) notFound();
658
727
 
659
728
  const { default: Post, metadata } = post;
@@ -667,18 +736,23 @@ export default async function BlogPostPage({
667
736
  </main>
668
737
  );
669
738
  }
670
- `, ASTRO_CONTENT_CONFIG = `import { glob } from "astro/loaders";
671
- 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";
672
750
 
673
751
  const blog = defineCollection({
674
- // Pattern includes both .md and .mdx so the collection is mixed-format.
675
- loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/blog" }),
676
- schema: z.object({
677
- title: z.string(),
678
- description: z.string(),
679
- pubDate: z.coerce.date(),
680
- updatedDate: z.coerce.date().optional(),
681
- }),
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,
682
756
  });
683
757
 
684
758
  export const collections = { blog };
@@ -756,6 +830,16 @@ const { Content } = await render(post);
756
830
  </html>
757
831
  `, markdown;
758
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
+ });
759
843
  markdown = {
760
844
  id: "markdown",
761
845
  kind: "content",
@@ -763,21 +847,25 @@ var init_markdown = __esm(() => {
763
847
  label: "MDX content",
764
848
  hint: "Markdown + JSX wired into the frontend (official recipe)"
765
849
  },
850
+ templates: [{ path: "packages/content/package.json", content: CONTENT_PACKAGE_JSON }],
766
851
  fragments: {
767
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 },
768
855
  { path: "apps/web/mdx-components.tsx", content: NEXT_MDX_COMPONENTS },
769
- { path: "apps/web/content/blog/hello.mdx", content: NEXT_BLOG_POST_MDX },
770
856
  { path: "apps/web/app/blog/posts.ts", content: NEXT_BLOG_POSTS_TS },
771
857
  { path: "apps/web/app/blog/page.tsx", content: NEXT_BLOG_INDEX_TSX },
772
858
  { path: "apps/web/app/blog/[slug]/page.tsx", content: NEXT_BLOG_SLUG_TSX }
773
859
  ],
774
860
  astro: [
861
+ { path: "packages/content/src/schema.ts", content: ASTRO_CONTENT_SCHEMA },
862
+ { path: "packages/content/blog/hello.mdx", content: ASTRO_BLOG_POST },
775
863
  { path: "apps/web/src/content.config.ts", content: ASTRO_CONTENT_CONFIG },
776
- { path: "apps/web/src/content/blog/hello.mdx", content: ASTRO_BLOG_POST },
777
864
  { path: "apps/web/src/pages/blog/index.astro", content: ASTRO_BLOG_INDEX },
778
865
  { path: "apps/web/src/pages/blog/[...slug].astro", content: ASTRO_BLOG_SLUG_PAGE }
779
866
  ]
780
867
  },
868
+ deps: [{ name: "@workspace/content", version: "workspace:*", target: "app" }],
781
869
  frontendDeps: {
782
870
  next: [
783
871
  { name: "@next/mdx", version: "^16.2.0", target: "app", dev: true },
@@ -787,17 +875,6 @@ var init_markdown = __esm(() => {
787
875
  ],
788
876
  astro: [{ name: "@astrojs/mdx", version: "^4.3.14", target: "app" }]
789
877
  },
790
- owns: [
791
- "apps/web/mdx-components.tsx",
792
- "apps/web/content/blog/hello.mdx",
793
- "apps/web/app/blog/posts.ts",
794
- "apps/web/app/blog/page.tsx",
795
- "apps/web/app/blog/[slug]/page.tsx",
796
- "apps/web/src/content.config.ts",
797
- "apps/web/src/content/blog/hello.mdx",
798
- "apps/web/src/pages/blog/index.astro",
799
- "apps/web/src/pages/blog/[...slug].astro"
800
- ],
801
878
  patches: [
802
879
  {
803
880
  path: "apps/web/next.config.ts",
@@ -813,204 +890,374 @@ var init_markdown = __esm(() => {
813
890
  };
814
891
  });
815
892
 
816
- // ../../packages/registry/src/content/sanity.ts
817
- var SCHEMA_BLOCKS = `import { defineArrayMember, defineField, defineType } from "sanity";
818
-
819
- // Page-builder blocks are objects, not documents: they only exist inside a page.
820
-
821
- export const hero = defineType({
822
- name: "hero",
823
- title: "Hero",
824
- type: "object",
825
- fields: [
826
- defineField({ name: "heading", type: "string", validation: (rule) => rule.required() }),
827
- defineField({ name: "subheading", type: "text", rows: 2 }),
828
- defineField({
829
- name: "image",
830
- type: "image",
831
- options: { hotspot: true },
832
- fields: [defineField({ name: "alt", type: "string", title: "Alternative text" })],
833
- }),
834
- ],
835
- preview: {
836
- select: { title: "heading", media: "image" },
837
- prepare: ({ title, media }) => ({ title: title ?? "Hero", subtitle: "Hero", media }),
838
- },
839
- });
840
-
841
- export const richText = defineType({
842
- name: "richText",
843
- title: "Rich text",
844
- type: "object",
845
- fields: [
846
- defineField({ name: "heading", type: "string" }),
847
- defineField({ name: "content", type: "array", of: [defineArrayMember({ type: "block" })] }),
848
- ],
849
- preview: {
850
- select: { title: "heading" },
851
- prepare: ({ title }) => ({ title: title ?? "Rich text", subtitle: "Rich text" }),
852
- },
853
- });
893
+ // ../../packages/registry/src/content/sanity/astro.ts
894
+ var ASTRO_ENV = `export const projectId = import.meta.env.PUBLIC_SANITY_PROJECT_ID || "placeholder";
854
895
 
855
- export const cta = defineType({
856
- name: "cta",
857
- title: "Call to action",
858
- type: "object",
859
- fields: [
860
- defineField({ name: "heading", type: "string", validation: (rule) => rule.required() }),
861
- defineField({ name: "text", type: "text", rows: 2 }),
862
- defineField({ name: "buttonLabel", type: "string" }),
863
- defineField({ name: "buttonHref", type: "string" }),
864
- ],
865
- preview: {
866
- select: { title: "heading" },
867
- prepare: ({ title }) => ({ title: title ?? "Call to action", subtitle: "Call to action" }),
868
- },
869
- });
870
- `, SCHEMA_PAGE = `import { defineArrayMember, defineField, defineType } from "sanity";
896
+ export const dataset = import.meta.env.PUBLIC_SANITY_DATASET || "production";
871
897
 
872
- export const page = defineType({
873
- name: "page",
874
- title: "Page",
875
- type: "document",
876
- fields: [
877
- defineField({ name: "title", type: "string", validation: (rule) => rule.required() }),
878
- defineField({
879
- name: "slug",
880
- type: "slug",
881
- options: { source: "title" },
882
- validation: (rule) => rule.required(),
883
- }),
884
- defineField({ name: "description", type: "text", rows: 3 }),
885
- defineField({
886
- name: "pageBuilder",
887
- title: "Page builder",
888
- type: "array",
889
- of: [
890
- defineArrayMember({ type: "hero" }),
891
- defineArrayMember({ type: "richText" }),
892
- defineArrayMember({ type: "cta" }),
893
- ],
894
- }),
895
- ],
896
- // The home page is the \`page\` whose slug is "home" — see sanity/queries.ts.
897
- preview: { select: { title: "title", subtitle: "slug.current" } },
898
- });
899
- `, 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 ?? "";
900
900
 
901
- export const settings = defineType({
902
- name: "settings",
903
- title: "Settings",
904
- type: "document",
905
- fields: [
906
- defineField({ name: "siteTitle", type: "string", validation: (rule) => rule.required() }),
907
- defineField({ name: "siteDescription", type: "text", rows: 3 }),
908
- defineField({
909
- name: "socialImage",
910
- title: "Social share image",
911
- type: "image",
912
- options: { hotspot: true },
913
- }),
914
- ],
915
- preview: { select: { title: "siteTitle", media: "socialImage" } },
916
- });
917
- `, SCHEMA_INDEX = `import type { SchemaTypeDefinition } from "sanity";
918
- import { cta, hero, richText } from "./blocks";
919
- import { page } from "./page";
920
- 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";
921
907
 
922
- export const schemaTypes: SchemaTypeDefinition[] = [page, settings, hero, richText, cta];
923
- `, 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";
924
915
 
925
916
  /**
926
- * \`settings\` is a singleton: one document with a fixed id, edited in place instead of
927
- * 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.
928
924
  *
929
- * The two sets feed sanity.config.ts's \`document.newDocumentOptions\` / \`document.actions\`
930
- * 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.
931
932
  */
932
- export const singletonTypes = new Set(["settings"]);
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";
933
960
 
934
- /** Actions that keep a singleton editable without allowing create/delete/duplicate. */
935
- export const singletonActions = new Set(["publish", "discardChanges", "restore"]);
961
+ const builder = createImageUrlBuilder({ projectId, dataset });
936
962
 
937
- export const structure: StructureResolver = (S) =>
938
- S.list()
939
- .title("Content")
940
- .items([
941
- S.listItem()
942
- .title("Settings")
943
- .id("settings")
944
- .child(S.document().schemaType("settings").documentId("settings")),
945
- S.divider(),
946
- S.documentTypeListItem("page").title("Pages"),
947
- ]);
948
- `, PRESENTATION = `import { defineDocuments, defineLocations } from "sanity/presentation";
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";
949
973
 
950
- /** Route document: lets Presentation open the right \`page\` for the URL in the iframe. */
951
- export const mainDocuments = defineDocuments([
952
- { route: "/", filter: \`_type == "page" && slug.current == "home"\` },
953
- { route: "/:slug", filter: \`_type == "page" && slug.current == $slug\` },
954
- ]);
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];
955
978
 
956
- /** Document → routes: the "Used on …" links shown when editing a document. */
957
- export const locations = {
958
- settings: defineLocations({
959
- locations: [{ title: "Home", href: "/" }],
960
- message: "This document is used on all pages",
961
- tone: "positive",
962
- }),
963
- page: defineLocations({
964
- select: { title: "title", slug: "slug.current" },
965
- resolve: (doc) => ({
966
- locations: [
967
- {
968
- title: doc?.title || "Untitled",
969
- href: doc?.slug === "home" ? "/" : \`/\${doc?.slug}\`,
970
- },
971
- ],
972
- }),
973
- }),
974
- };
975
- `, DOCUMENT_GUARD = ` document: {
976
- newDocumentOptions: (prev, { creationContext }) =>
977
- creationContext.type === "global"
978
- ? prev.filter((template) => !singletonTypes.has(template.templateId))
979
- : prev,
980
- actions: (prev, { schemaType }) =>
981
- singletonTypes.has(schemaType)
982
- ? prev.filter(({ action }) => action && singletonActions.has(action))
983
- : prev,
984
- },`, QUERIES = `import { defineQuery } from "groq";
979
+ interface Props {
980
+ blocks: PageBlock[];
981
+ }
985
982
 
986
- const PAGE_FIELDS = /* groq */ \`
987
- _id,
988
- _type,
989
- title,
990
- description,
991
- "slug": slug.current,
992
- pageBuilder[]{ ... }
993
- \`;
983
+ const { blocks } = Astro.props;
994
984
 
995
- export const HOME_PAGE_QUERY = defineQuery(
996
- \`*[_type == "page" && slug.current == "home"][0]{\${PAGE_FIELDS}}\`,
997
- );
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;";
998
988
 
999
- export const PAGE_QUERY = defineQuery(
1000
- \`*[_type == "page" && slug.current == $slug][0]{\${PAGE_FIELDS}}\`,
1001
- );
989
+ const buttonProps = (button: ButtonLink) =>
990
+ button.openInNewTab ? { target: "_blank", rel: "noreferrer" } : {};
991
+ ---
1002
992
 
1003
- export const PAGE_SLUGS_QUERY = defineQuery(
1004
- \`*[_type == "page" && defined(slug.current) && slug.current != "home"].slug.current\`,
1005
- );
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";
1006
1139
 
1007
- export const SETTINGS_QUERY = defineQuery(
1008
- \`*[_type == "settings"][0]{ siteTitle, siteDescription, socialImage }\`,
1009
- );
1010
- `, SEED_NDJSON = `{"_id":"settings","_type":"settings","siteTitle":"Turbo Wizard","siteDescription":"A Turborepo monorepo scaffolded with create-turbo-wizard, with Sanity wired end to end."}
1011
- {"_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"}]}
1012
- {"_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."}]}]}]}
1013
- `, NEXT_ENV = `export const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID || "placeholder";
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";
1014
1261
 
1015
1262
  export const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET || "production";
1016
1263
 
@@ -1021,8 +1268,12 @@ export const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION || "2026-02
1021
1268
  /** Server-only read token; empty when unset (draft mode and live previews need it). */
1022
1269
  export const readToken = process.env.SANITY_API_READ_TOKEN ?? "";
1023
1270
 
1024
- /** The embedded Studio's route — stega encodes it into every string it returns. */
1025
- export const studioUrl = "/studio";
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";
1026
1277
 
1027
1278
  /**
1028
1279
  * False on a freshly scaffolded project. Guards every fetch so the build never dials
@@ -1043,17 +1294,33 @@ export const client = createClient({
1043
1294
  stega: { studioUrl },
1044
1295
  });
1045
1296
  `, NEXT_LIVE = `import { defineLive } from "next-sanity/live";
1046
- import { readToken } from "./env";
1297
+ import { hasSanityCredentials, readToken } from "./env";
1047
1298
  import { client } from "./client";
1048
1299
 
1049
1300
  // defineLive gives us live content plus draft-mode reads. The token is passed as
1050
1301
  // undefined rather than "" when unset — an empty string reads as "provided but invalid".
1051
- export const { sanityFetch, SanityLive } = defineLive({
1302
+ export const { sanityFetch, SanityLive: LiveComponent } = defineLive({
1052
1303
  client,
1053
1304
  serverToken: readToken || undefined,
1054
1305
  browserToken: readToken || undefined,
1055
1306
  });
1056
- `, 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";
1057
1324
  import { client } from "./client";
1058
1325
  import { hasSanityCredentials } from "./env";
1059
1326
  import { sanityFetch } from "./live";
@@ -1107,52 +1374,234 @@ export function urlFor(source: unknown, width = 1200): string | null {
1107
1374
  return builder.image(ref).width(width).auto("format").url();
1108
1375
  }
1109
1376
  `, NEXT_PAGE_BUILDER = `import { PortableText, type PortableTextProps } from "@portabletext/react";
1377
+ import { urlFor } from "@workspace/sanity/image";
1110
1378
  import Image from "next/image";
1111
- import { urlFor } from "./image";
1112
1379
 
1113
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
+ };
1114
1391
 
1115
1392
  export type PageBlock =
1116
- | { _key: string; _type: "hero"; heading?: string; subheading?: string; image?: SanityImage }
1117
- | { _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
+ }
1118
1452
  | {
1119
1453
  _key: string;
1120
1454
  _type: "cta";
1121
- heading?: string;
1122
- text?: string;
1123
- buttonLabel?: string;
1124
- buttonHref?: string;
1455
+ eyebrow?: string | null;
1456
+ title?: string | null;
1457
+ richText?: Rich;
1458
+ buttons?: ButtonLink[] | null;
1125
1459
  };
1126
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
+
1127
1492
  function Block({ block }: { block: PageBlock }) {
1128
1493
  switch (block._type) {
1129
1494
  case "hero": {
1130
1495
  const src = urlFor(block.image, 1600);
1131
1496
  return (
1132
- <section>
1133
- <h2>{block.heading}</h2>
1134
- {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} />
1135
1502
  {src ? (
1136
1503
  <Image src={src} alt={block.image?.alt ?? ""} width={1600} height={900} />
1137
1504
  ) : null}
1138
1505
  </section>
1139
1506
  );
1140
1507
  }
1141
- 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.
1142
1587
  return (
1143
- <section>
1144
- {block.heading ? <h2>{block.heading}</h2> : null}
1145
- {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}
1146
1596
  </section>
1147
1597
  );
1148
1598
  case "cta":
1149
1599
  return (
1150
- <section>
1151
- <h2>{block.heading}</h2>
1152
- {block.text ? <p>{block.text}</p> : null}
1153
- {block.buttonHref && block.buttonLabel ? (
1154
- <a href={block.buttonHref}>{block.buttonLabel}</a>
1155
- ) : 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} />
1156
1605
  </section>
1157
1606
  );
1158
1607
  default:
@@ -1170,34 +1619,9 @@ export function PageBuilder({ blocks }: { blocks: PageBlock[] }) {
1170
1619
  </>
1171
1620
  );
1172
1621
  }
1173
- `, NEXT_SANITY_CONFIG, NEXT_SANITY_CLI = `import { defineCliConfig } from "sanity/cli";
1174
-
1175
- // Run from apps/web. \`schema extract\` + \`typegen generate\` need no credentials, so the
1176
- // build-time \`prebuild\` hook works on a fresh clone.
1177
- export default defineCliConfig({
1178
- api: {
1179
- projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID || "placeholder",
1180
- dataset: process.env.NEXT_PUBLIC_SANITY_DATASET || "production",
1181
- },
1182
- typegen: {
1183
- path: "./sanity/**/*.{ts,tsx}",
1184
- schema: "sanity/schema.json",
1185
- generates: "sanity/sanity.types.ts",
1186
- },
1187
- });
1188
- `, NEXT_STUDIO_PAGE = `import { NextStudio } from "next-sanity/studio";
1189
- import config from "../../../sanity.config";
1190
-
1191
- export const dynamic = "force-static";
1192
-
1193
- export { metadata, viewport } from "next-sanity/studio";
1194
-
1195
- export default function StudioPage() {
1196
- return <NextStudio config={config} />;
1197
- }
1198
- `, NEXT_DRAFT_MODE_ROUTE = `import { defineEnableDraftMode } from "next-sanity/draft-mode";
1199
- import { readToken } from "../../../../sanity/env";
1200
- 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";
1201
1625
 
1202
1626
  export const { GET } = defineEnableDraftMode({
1203
1627
  client: client.withConfig({ token: readToken }),
@@ -1235,10 +1659,10 @@ export function DisableDraftMode() {
1235
1659
  </a>
1236
1660
  );
1237
1661
  }
1238
- `, NEXT_HOME_PAGE = `import type { Metadata } from "next";
1239
- import { loadQuery } from "../sanity/fetch";
1240
- import { PageBuilder, type PageBlock } from "../sanity/page-builder";
1241
- 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";
1242
1666
 
1243
1667
  // stega: false — encoded metadata would leak invisible characters into <title>.
1244
1668
  export async function generateMetadata(): Promise<Metadata> {
@@ -1258,7 +1682,8 @@ export default async function HomePage() {
1258
1682
  <h1>No content yet</h1>
1259
1683
  <p>
1260
1684
  Set NEXT_PUBLIC_SANITY_PROJECT_ID in apps/web/.env.local, then seed the dataset:
1261
- <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>.
1262
1687
  </p>
1263
1688
  </main>
1264
1689
  );
@@ -1271,10 +1696,10 @@ export default async function HomePage() {
1271
1696
  </main>
1272
1697
  );
1273
1698
  }
1274
- `, NEXT_SLUG_PAGE = `import { notFound } from "next/navigation";
1275
- import { loadQuery, loadStaticQuery } from "../../sanity/fetch";
1276
- import { PageBuilder, type PageBlock } from "../../sanity/page-builder";
1277
- 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";
1278
1703
 
1279
1704
  export async function generateStaticParams() {
1280
1705
  const slugs = await loadStaticQuery(PAGE_SLUGS_QUERY);
@@ -1293,193 +1718,1877 @@ export default async function Page({ params }: { params: Promise<{ slug: string
1293
1718
  </main>
1294
1719
  );
1295
1720
  }
1296
- `, 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";
1297
1726
 
1298
- export const dataset = import.meta.env.PUBLIC_SANITY_DATASET || "production";
1727
+ export const metadata: Metadata = { title: "Blog" };
1299
1728
 
1300
- /** Server-only read token; required (and only read) when Visual Editing is enabled. */
1301
- export const readToken = import.meta.env.SANITY_API_READ_TOKEN ?? "";
1729
+ export default async function BlogIndexPage() {
1730
+ const posts = (await loadQuery(BLOG_INDEX_QUERY)) ?? [];
1302
1731
 
1303
- /**
1304
- * The @sanity/astro Visual Editing switch. Off by default: overlays need a rendering
1305
- * server (\`astro dev\`, or an SSR adapter in production) plus SANITY_API_READ_TOKEN.
1306
- */
1307
- export const visualEditingEnabled =
1308
- 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";
1309
1769
 
1310
- /**
1311
- * False on a freshly scaffolded project. Guards every fetch so \`astro build\` never dials
1312
- * placeholder.api.sanity.io and dies; pages render an empty state instead.
1313
- */
1314
- export const hasSanityCredentials = projectId !== "placeholder";
1315
- `, ASTRO_FETCH = `import { sanityClient } from "sanity:client";
1316
- 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";
1814
+
1815
+ /**
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.
2308
+ *
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.
2383
+ */
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} } }
2405
+ }
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
+ });
1317
2704
 
1318
- /**
1319
- * The single read path, following @sanity/astro's Visual Editing recipe: with
1320
- * PUBLIC_SANITY_VISUAL_EDITING_ENABLED=true it fetches the drafts perspective with
1321
- * stega source maps, which is what gives Presentation its click-to-edit overlays.
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\`
1322
2808
  *
1323
- * Without credentials it returns null before touching the network, so \`astro build\`
1324
- * succeeds on a fresh scaffold. The catch covers a configured-but-unreachable dataset
1325
- * for the same reason: a starter should render empty, not crash.
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
+ * ---------------------------------------------------------------------------------
1326
2816
  */
1327
- export async function loadQuery<T>(
1328
- query: string,
1329
- params: Record<string, unknown> = {},
1330
- ): Promise<T | null> {
1331
- if (!hasSanityCredentials) return null;
1332
- if (visualEditingEnabled && !readToken) {
1333
- throw new Error(
1334
- "SANITY_API_READ_TOKEN is required when PUBLIC_SANITY_VISUAL_EDITING_ENABLED is true.",
1335
- );
1336
- }
1337
- try {
1338
- const { result } = await sanityClient.fetch<T>(query, params, {
1339
- filterResponse: false,
1340
- perspective: visualEditingEnabled ? "drafts" : "published",
1341
- resultSourceMap: visualEditingEnabled ? "withKeyArraySelector" : false,
1342
- stega: visualEditingEnabled,
1343
- ...(visualEditingEnabled ? { token: readToken } : {}),
1344
- useCdn: !visualEditingEnabled,
1345
- });
1346
- return result;
1347
- } catch (error) {
1348
- console.warn("[sanity] query failed:", error);
1349
- return null;
1350
- }
1351
- }
1352
- `, ASTRO_IMAGE = `import { createImageUrlBuilder } from "@sanity/image-url";
1353
- import { dataset, projectId } from "./env";
1354
2817
 
1355
- const builder = createImageUrlBuilder({ projectId, dataset });
2818
+ export declare const internalGroqTypeReferenceTo: unique symbol;
1356
2819
 
1357
- /** Build a CDN URL for a Sanity image; null when the block has no asset. */
1358
- export function urlFor(source: unknown, width = 1200): string | null {
1359
- const ref = (source as { asset?: { _ref?: string } } | null)?.asset?._ref;
1360
- if (!ref) return null;
1361
- return builder.image(ref).width(width).auto("format").url();
1362
- }
1363
- `, ASTRO_TYPES = `import type { PortableTextBlock } from "@portabletext/types";
2820
+ // Source: schema.json
2821
+ export type SanityImageAssetReference = {
2822
+ _ref: string;
2823
+ _type: "reference";
2824
+ _weak?: boolean;
2825
+ [internalGroqTypeReferenceTo]?: "sanity.imageAsset";
2826
+ };
1364
2827
 
1365
- type SanityImage = { asset?: { _ref?: string } | null; alt?: string | null } | null;
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
+ };
1366
2836
 
1367
- export type PageBlock =
1368
- | { _key: string; _type: "hero"; heading?: string; subheading?: string; image?: SanityImage }
1369
- | { _key: string; _type: "richText"; heading?: string; content?: PortableTextBlock[] }
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<
1370
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";
1371
2946
  _key: string;
1372
- _type: "cta";
1373
- heading?: string;
1374
- text?: string;
1375
- buttonLabel?: string;
1376
- buttonHref?: string;
1377
- };
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
+ };
2987
+
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
+ };
3004
+
3005
+ export type SanityImageCrop = {
3006
+ _type: "sanity.imageCrop";
3007
+ top: number;
3008
+ bottom: number;
3009
+ left: number;
3010
+ right: number;
3011
+ };
3012
+
3013
+ export type SanityImageHotspot = {
3014
+ _type: "sanity.imageHotspot";
3015
+ x: number;
3016
+ y: number;
3017
+ height: number;
3018
+ width: number;
3019
+ };
3020
+
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
+ {
3067
+ _key: string;
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
+ };
3166
+
3167
+ export type SanityFileAsset = {
3168
+ _id: string;
3169
+ _type: "sanity.fileAsset";
3170
+ _createdAt: string;
3171
+ _updatedAt: string;
3172
+ _rev: string;
3173
+ originalFilename?: string;
3174
+ label?: string;
3175
+ title?: string;
3176
+ description?: string;
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
+ };
3188
+
3189
+ export type SanityAssetSourceData = {
3190
+ _type: "sanity.assetSourceData";
3191
+ name?: string;
3192
+ id?: string;
3193
+ url?: string;
3194
+ };
3195
+
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
+ };
3218
+
3219
+ export type Geopoint = {
3220
+ _type: "geopoint";
3221
+ lat?: number;
3222
+ lng?: number;
3223
+ alt?: number;
3224
+ };
3225
+
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;
1378
3372
 
1379
- export type SanityPage = {
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 = {
1380
3377
  _id: string;
1381
- title?: string;
1382
- description?: string;
1383
- slug?: string;
1384
- pageBuilder?: PageBlock[] | null;
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;
1385
3484
  } | null;
1386
- `, ASTRO_PAGE_BUILDER = `---
1387
- import { PortableText } from "astro-portabletext";
1388
- import type { PageBlock } from "../../sanity/types";
1389
- import { urlFor } from "../../sanity/image";
1390
3485
 
1391
- interface Props {
1392
- blocks: PageBlock[];
1393
- }
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>;
1394
3490
 
1395
- const { blocks } = Astro.props;
1396
- ---
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;
1397
3553
 
1398
- {
1399
- blocks.map((block) => {
1400
- if (block._type === "hero") {
1401
- const src = urlFor(block.image, 1600);
1402
- return (
1403
- <section>
1404
- <h2>{block.heading}</h2>
1405
- {block.subheading && <p>{block.subheading}</p>}
1406
- {src && <img src={src} alt={block.image?.alt ?? ""} />}
1407
- </section>
1408
- );
1409
- }
1410
- if (block._type === "richText") {
1411
- return (
1412
- <section>
1413
- {block.heading && <h2>{block.heading}</h2>}
1414
- {block.content && <PortableText value={block.content} />}
1415
- </section>
1416
- );
1417
- }
1418
- if (block._type === "cta") {
1419
- return (
1420
- <section>
1421
- <h2>{block.heading}</h2>
1422
- {block.text && <p>{block.text}</p>}
1423
- {block.buttonHref && block.buttonLabel && (
1424
- <a href={block.buttonHref}>{block.buttonLabel}</a>
1425
- )}
1426
- </section>
1427
- );
1428
- }
1429
- return null;
1430
- })
1431
- }
1432
- `, ASTRO_SLUG_PAGE = `---
1433
- import { VisualEditing } from "@sanity/astro/visual-editing";
1434
- import PageBuilder from "../components/PageBuilder.astro";
1435
- import { visualEditingEnabled } from "../../sanity/env";
1436
- import { loadQuery } from "../../sanity/fetch";
1437
- import { PAGE_QUERY, PAGE_SLUGS_QUERY } from "../../sanity/queries";
1438
- import type { SanityPage } from "../../sanity/types";
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;
1439
3573
 
1440
- export async function getStaticPaths() {
1441
- // Returns [] without credentials, so a fresh scaffold builds with no CMS pages.
1442
- const slugs = await loadQuery<(string | null)[]>(PAGE_SLUGS_QUERY);
1443
- return (slugs ?? []).filter(Boolean).map((slug) => ({ params: { slug } }));
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
+ }
1444
3586
  }
3587
+ `;
1445
3588
 
1446
- const page = await loadQuery<SanityPage>(PAGE_QUERY, { slug: Astro.params.slug });
1447
- ---
1448
-
1449
- <html lang="en">
1450
- <head>
1451
- <meta charset="utf-8" />
1452
- <meta name="viewport" content="width=device-width, initial-scale=1" />
1453
- <title>{page?.title ?? "web"}</title>
1454
- </head>
1455
- <body style="font-family: system-ui, sans-serif; padding: 3rem;">
1456
- <h1>{page?.title}</h1>
1457
- <PageBuilder blocks={page?.pageBuilder ?? []} />
1458
- <VisualEditing enabled={visualEditingEnabled} />
1459
- </body>
1460
- </html>
1461
- `, ASTRO_SANITY_CONFIG, ASTRO_SANITY_CLI = `import { defineCliConfig } from "sanity/cli";
1462
-
1463
- // Run from apps/web. The Sanity CLI is a Node process, so it reads process.env rather
1464
- // than Astro's import.meta.env; astro.config.mjs loads apps/web/.env into it.
1465
- export default defineCliConfig({
1466
- api: {
1467
- projectId: process.env.PUBLIC_SANITY_PROJECT_ID || "placeholder",
1468
- dataset: process.env.PUBLIC_SANITY_DATASET || "production",
1469
- },
1470
- typegen: {
1471
- path: "./sanity/**/*.{ts,tsx}",
1472
- schema: "sanity/schema.json",
1473
- generates: "sanity/sanity.types.ts",
1474
- },
1475
- });
1476
- `, 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.
1477
- try {
1478
- process.loadEnvFile(".env");
1479
- } catch {
1480
- // No .env — the placeholder ids below keep the build working.
1481
- }
1482
- `, prependAstroConfigStatement = (content, block, marker) => content.includes(marker) ? content : content.replace("export default defineConfig({", `${block}
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}
1483
3592
  export default defineConfig({`), addAstroOptimizeDeps = (content) => {
1484
3593
  if (content.includes("react/compiler-runtime"))
1485
3594
  return content;
@@ -1496,23 +3605,12 @@ export default defineConfig({`), addAstroOptimizeDeps = (content) => {
1496
3605
  return content.replace(/vite:\s*\{/, (m) => `${m} ${field}`);
1497
3606
  return content.replace(/defineConfig\(\{/, (m) => `${m}
1498
3607
  vite: { ${field} },`);
1499
- }, addScripts = (pkgJson, scripts) => {
1500
- const pkg = JSON.parse(pkgJson);
1501
- const merged = { ...pkg.scripts, ...scripts };
1502
- const sorted = Object.fromEntries(Object.entries(merged).sort(([a], [b]) => a.localeCompare(b)));
1503
- return `${JSON.stringify({ ...pkg, scripts: sorted }, null, 2)}
1504
- `;
1505
- }, TYPEGEN = "sanity schema extract --enforce-required-fields --path sanity/schema.json && sanity typegen generate", patchWebPackageJson = (content) => addScripts(content, {
1506
- "sanity:typegen": TYPEGEN,
1507
- prebuild: TYPEGEN,
1508
- ...content.includes('"next build"') ? { "precheck-types": TYPEGEN } : {},
1509
- "sanity:seed": "sanity dataset import sanity/seed.ndjson"
1510
- }), 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) => {
1511
3609
  const withImports = [
1512
3610
  'import { draftMode } from "next/headers";',
1513
3611
  'import { VisualEditing } from "next-sanity/visual-editing";',
1514
3612
  'import { DisableDraftMode } from "../sanity/disable-draft-mode";',
1515
- 'import { SanityLive } from "../sanity/live";'
3613
+ 'import { SanityLive } from "@workspace/sanity/live";'
1516
3614
  ].reduce(ensureImport, content);
1517
3615
  const asAsync = replaceOnce(withImports, NEXT_LAYOUT_SIGNATURE, `export default async function RootLayout({ children }: { children: ReactNode }) {
1518
3616
  const { isEnabled: isDraft } = await draftMode();`);
@@ -1534,15 +3632,15 @@ export default defineConfig({`), addAstroOptimizeDeps = (content) => {
1534
3632
  return content;
1535
3633
  const withImports = [
1536
3634
  'import { VisualEditing } from "@sanity/astro/visual-editing";',
1537
- 'import PageBuilder from "../components/PageBuilder.astro";',
1538
- 'import { visualEditingEnabled } from "../../sanity/env";',
1539
- 'import { loadQuery } from "../../sanity/fetch";',
1540
- 'import { HOME_PAGE_QUERY } from "../../sanity/queries";',
1541
- '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";'
1542
3640
  ].reduce(ensureImport, content);
1543
3641
  const withFetch = replaceOnce(withImports, 'log.info("rendering home");', `log.info("rendering home");
1544
3642
 
1545
- const page = await loadQuery<SanityPage>(HOME_PAGE_QUERY);`);
3643
+ const page = await loadQuery<HOME_PAGE_QUERY_RESULT>(HOME_PAGE_QUERY);`);
1546
3644
  const withHeading = replaceOnce(withFetch, " <h1>create-turbo-wizard</h1>", ' <h1>{page?.title ?? "create-turbo-wizard"}</h1>');
1547
3645
  const withBlocks = replaceOnce(withHeading, " <p>Astro + Turborepo, scaffolded.</p>", [
1548
3646
  " <PageBuilder blocks={page?.pageBuilder ?? []} />",
@@ -1556,136 +3654,142 @@ const page = await loadQuery<SanityPage>(HOME_PAGE_QUERY);`);
1556
3654
  }, README_SECTION = `
1557
3655
  ## Sanity
1558
3656
 
1559
- The Studio is embedded in \`apps/web\` Next serves it at \`/studio\`, Astro at \`/admin\`.
1560
- 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\`.
1561
3661
 
1562
3662
  \`\`\`sh
1563
- cd apps/web
1564
- cp ../../.env.example .env.local # Astro: .env — then fill in the project id
1565
- pnpm sanity:seed # import sanity/seed.ndjson into the configured dataset
1566
- 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
1567
3668
  \`\`\`
1568
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
+
1569
3673
  Without a project id the site builds and serves an empty state — no credentials required.
1570
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
+
1571
3687
  ### Visual Editing
1572
3688
 
1573
3689
  Next: open the Studio's **Presentation** tab — draft mode enables via
1574
3690
  \`/api/draft-mode/enable\`; leave it with the on-page "Disable draft mode" button.
1575
3691
  Astro: set \`PUBLIC_SANITY_VISUAL_EDITING_ENABLED=true\` and \`SANITY_API_READ_TOKEN\`,
1576
3692
  then use the dev server — overlays need server rendering (add an SSR adapter for a
1577
- 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.
1578
3695
  `, sanity;
1579
3696
  var init_sanity = __esm(() => {
1580
- NEXT_SANITY_CONFIG = `"use client";
1581
-
1582
- import { defineConfig } from "sanity";
1583
- import { presentationTool } from "sanity/presentation";
1584
- import { structureTool } from "sanity/structure";
1585
- import { locations, mainDocuments } from "./sanity/presentation";
1586
- import { schemaTypes } from "./sanity/schema/index";
1587
- import { singletonActions, singletonTypes, structure } from "./sanity/structure";
1588
-
1589
- export default defineConfig({
1590
- name: "default",
1591
- title: "Studio",
1592
- basePath: "/studio",
1593
- projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID || "placeholder",
1594
- dataset: process.env.NEXT_PUBLIC_SANITY_DATASET || "production",
1595
- plugins: [
1596
- presentationTool({
1597
- resolve: { mainDocuments, locations },
1598
- previewUrl: {
1599
- initial: "/",
1600
- previewMode: {
1601
- enable: "/api/draft-mode/enable",
1602
- // Presentation never calls this itself (documented as not yet implemented);
1603
- // editors leave draft mode via <DisableDraftMode/>, which links to the route.
1604
- disable: "/api/draft-mode/disable",
1605
- },
1606
- },
1607
- }),
1608
- structureTool({ structure }),
1609
- ],
1610
- schema: { types: schemaTypes },
1611
- ${DOCUMENT_GUARD}
1612
- });
1613
- `;
1614
- ASTRO_SANITY_CONFIG = `import { defineConfig } from "sanity";
1615
- import { presentationTool } from "sanity/presentation";
1616
- import { structureTool } from "sanity/structure";
1617
- import { locations, mainDocuments } from "./sanity/presentation";
1618
- import { schemaTypes } from "./sanity/schema/index";
1619
- import { singletonActions, singletonTypes, structure } from "./sanity/structure";
1620
-
1621
- export default defineConfig({
1622
- name: "default",
1623
- title: "Studio",
1624
- projectId: import.meta.env.PUBLIC_SANITY_PROJECT_ID || "placeholder",
1625
- dataset: import.meta.env.PUBLIC_SANITY_DATASET || "production",
1626
- plugins: [
1627
- presentationTool({
1628
- resolve: { mainDocuments, locations },
1629
- // No previewMode: Astro has no draft-mode routes. Draft content comes from
1630
- // loadQuery's perspective switch (PUBLIC_SANITY_VISUAL_EDITING_ENABLED + token),
1631
- // which needs a rendering server — \`astro dev\`, or an SSR adapter in production.
1632
- previewUrl: { initial: "/" },
1633
- }),
1634
- structureTool({ structure }),
1635
- ],
1636
- schema: { types: schemaTypes },
1637
- ${DOCUMENT_GUARD}
1638
- });
1639
- `;
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
+ });
1640
3725
  sanity = {
1641
3726
  id: "sanity",
1642
3727
  kind: "content",
1643
3728
  prompt: {
1644
3729
  label: "Sanity",
1645
- hint: "Headless CMS with an embedded Studio (Next: /studio, Astro: /admin)"
3730
+ hint: "Headless CMS with a Studio of its own (apps/studio)"
1646
3731
  },
1647
3732
  templates: [
1648
- { path: "apps/web/sanity/schema/blocks.ts", content: SCHEMA_BLOCKS },
1649
- { path: "apps/web/sanity/schema/page.ts", content: SCHEMA_PAGE },
1650
- { path: "apps/web/sanity/schema/settings.ts", content: SCHEMA_SETTINGS },
1651
- { path: "apps/web/sanity/schema/index.ts", content: SCHEMA_INDEX },
1652
- { path: "apps/web/sanity/structure.ts", content: STRUCTURE },
1653
- { path: "apps/web/sanity/presentation.ts", content: PRESENTATION },
1654
- { path: "apps/web/sanity/queries.ts", content: QUERIES },
1655
- { 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 }
1656
3749
  ],
1657
3750
  fragments: {
1658
3751
  next: [
1659
- { path: "apps/web/sanity/env.ts", content: NEXT_ENV },
1660
- { path: "apps/web/sanity/client.ts", content: NEXT_CLIENT },
1661
- { path: "apps/web/sanity/live.ts", content: NEXT_LIVE },
1662
- { path: "apps/web/sanity/fetch.ts", content: NEXT_FETCH },
1663
- { path: "apps/web/sanity/image.ts", content: NEXT_IMAGE },
1664
- { 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 },
1665
3760
  { path: "apps/web/sanity/disable-draft-mode.tsx", content: NEXT_DISABLE_DRAFT_MODE },
1666
- { path: "apps/web/sanity.config.ts", content: NEXT_SANITY_CONFIG },
1667
- { path: "apps/web/sanity.cli.ts", content: NEXT_SANITY_CLI },
1668
- { path: "apps/web/app/studio/[[...tool]]/page.tsx", content: NEXT_STUDIO_PAGE },
1669
3761
  { path: "apps/web/app/api/draft-mode/enable/route.ts", content: NEXT_DRAFT_MODE_ROUTE },
1670
3762
  {
1671
3763
  path: "apps/web/app/api/draft-mode/disable/route.ts",
1672
3764
  content: NEXT_DRAFT_MODE_DISABLE_ROUTE
1673
3765
  },
1674
- { 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 }
1675
3769
  ],
1676
3770
  astro: [
1677
- { path: "apps/web/sanity/env.ts", content: ASTRO_ENV },
1678
- { path: "apps/web/sanity/fetch.ts", content: ASTRO_FETCH },
1679
- { path: "apps/web/sanity/image.ts", content: ASTRO_IMAGE },
1680
- { path: "apps/web/sanity/types.ts", content: ASTRO_TYPES },
1681
- { path: "apps/web/sanity.config.ts", content: ASTRO_SANITY_CONFIG },
1682
- { path: "apps/web/sanity.cli.ts", content: ASTRO_SANITY_CLI },
1683
- { path: "apps/web/src/components/PageBuilder.astro", content: ASTRO_PAGE_BUILDER },
1684
- { 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 }
1685
3780
  ]
1686
3781
  },
3782
+ scripts: {
3783
+ "dev:studio": "turbo run dev --filter=studio",
3784
+ "build:studio": "turbo run build --filter=studio"
3785
+ },
1687
3786
  deps: [
1688
- { 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" }
1689
3793
  ],
1690
3794
  frontendDeps: {
1691
3795
  next: [
@@ -1693,59 +3797,40 @@ ${DOCUMENT_GUARD}
1693
3797
  { name: "@portabletext/react", version: "^7.0.1", target: "app" },
1694
3798
  { name: "@sanity/image-url", version: "^2.1.1", target: "app" },
1695
3799
  { name: "@sanity/client", version: "^7.26.0", target: "app" },
1696
- { name: "sanity", version: "^5.31.1", target: "app" },
1697
- { 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" }
1698
3806
  ],
1699
3807
  astro: [
1700
3808
  { name: "@sanity/astro", version: "^3.5.0", target: "app" },
1701
3809
  { name: "astro-portabletext", version: "^0.13.0", target: "app" },
1702
- { name: "@portabletext/types", version: "^4.0.2", target: "app" },
1703
3810
  { name: "@sanity/image-url", version: "^2.1.1", target: "app" },
1704
3811
  { name: "@sanity/client", version: "^7.26.0", target: "app" },
1705
- { name: "sanity", version: "^5.31.1", target: "app" },
1706
3812
  { name: "@sanity/ui", version: "^3.5.1", target: "app" },
1707
3813
  { name: "react-compiler-runtime", version: "^1.0.0", target: "app" },
1708
3814
  { name: "react-is", version: "^19.0.0", target: "app" },
1709
3815
  { name: "lodash", version: "^4.17.21", target: "app" },
1710
- { name: "styled-components", version: "^6.1", target: "app" },
1711
- { 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" }
1712
3818
  ]
1713
3819
  },
1714
3820
  env: [
1715
3821
  { key: "NEXT_PUBLIC_SANITY_PROJECT_ID", scope: "client" },
1716
3822
  { key: "NEXT_PUBLIC_SANITY_DATASET", scope: "client" },
1717
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" },
1718
3828
  { key: "SANITY_API_READ_TOKEN", scope: "server" },
1719
3829
  { key: "PUBLIC_SANITY_PROJECT_ID", scope: "client" },
1720
3830
  { key: "PUBLIC_SANITY_DATASET", scope: "client" },
3831
+ { key: "PUBLIC_SANITY_STUDIO_URL", scope: "client" },
1721
3832
  { key: "PUBLIC_SANITY_VISUAL_EDITING_ENABLED", scope: "client" }
1722
3833
  ],
1723
- owns: [
1724
- "apps/web/sanity/schema/blocks.ts",
1725
- "apps/web/sanity/schema/page.ts",
1726
- "apps/web/sanity/schema/settings.ts",
1727
- "apps/web/sanity/schema/index.ts",
1728
- "apps/web/sanity/structure.ts",
1729
- "apps/web/sanity/presentation.ts",
1730
- "apps/web/sanity/queries.ts",
1731
- "apps/web/sanity/seed.ndjson",
1732
- "apps/web/sanity/env.ts",
1733
- "apps/web/sanity/client.ts",
1734
- "apps/web/sanity/live.ts",
1735
- "apps/web/sanity/fetch.ts",
1736
- "apps/web/sanity/image.ts",
1737
- "apps/web/sanity/types.ts",
1738
- "apps/web/sanity/page-builder.tsx",
1739
- "apps/web/sanity/disable-draft-mode.tsx",
1740
- "apps/web/sanity.config.ts",
1741
- "apps/web/sanity.cli.ts",
1742
- "apps/web/app/studio/[[...tool]]/page.tsx",
1743
- "apps/web/app/api/draft-mode/enable/route.ts",
1744
- "apps/web/app/api/draft-mode/disable/route.ts",
1745
- "apps/web/app/[slug]/page.tsx",
1746
- "apps/web/src/components/PageBuilder.astro",
1747
- "apps/web/src/pages/[slug].astro"
1748
- ],
1749
3834
  patches: [
1750
3835
  {
1751
3836
  path: "apps/web/next.config.ts",
@@ -1755,14 +3840,13 @@ ${DOCUMENT_GUARD}
1755
3840
  { path: "apps/web/app/page.tsx", apply: replaceHomePage(NEXT_HOME_PAGE) },
1756
3841
  {
1757
3842
  path: "apps/web/astro.config.mjs",
1758
- 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' } })`))
1759
3844
  },
1760
3845
  {
1761
3846
  path: "apps/web/src/env.d.ts",
1762
3847
  apply: (content) => ensureImport(content, ASTRO_MODULE_REF)
1763
3848
  },
1764
3849
  { path: "apps/web/src/pages/index.astro", apply: patchAstroIndex },
1765
- { path: "apps/web/package.json", apply: patchWebPackageJson },
1766
3850
  {
1767
3851
  path: "README.md",
1768
3852
  apply: (content) => content.includes("## Sanity") ? content : content + README_SECTION
@@ -1772,7 +3856,7 @@ ${DOCUMENT_GUARD}
1772
3856
  });
1773
3857
 
1774
3858
  // ../../packages/registry/src/frontends/astro.ts
1775
- var j4 = (value) => `${JSON.stringify(value, null, 2)}
3859
+ var j7 = (value) => `${JSON.stringify(value, null, 2)}
1776
3860
  `, ASTRO_CONFIG = `import react from "@astrojs/react";
1777
3861
  import { defineConfig } from "astro/config";
1778
3862
 
@@ -1805,7 +3889,7 @@ var init_astro = __esm(() => {
1805
3889
  templates: [
1806
3890
  {
1807
3891
  path: "apps/web/package.json",
1808
- content: j4({
3892
+ content: j7({
1809
3893
  name: "web",
1810
3894
  version: "0.0.0",
1811
3895
  private: true,
@@ -1837,7 +3921,7 @@ var init_astro = __esm(() => {
1837
3921
  { path: "apps/web/astro.config.mjs", content: ASTRO_CONFIG },
1838
3922
  {
1839
3923
  path: "apps/web/tsconfig.json",
1840
- content: j4({
3924
+ content: j7({
1841
3925
  extends: "astro/tsconfigs/strict",
1842
3926
  compilerOptions: { jsx: "react-jsx", jsxImportSource: "react" },
1843
3927
  include: [".astro/types.d.ts", "**/*"],
@@ -1847,17 +3931,44 @@ var init_astro = __esm(() => {
1847
3931
  { path: "apps/web/src/env.d.ts", content: `/// <reference types="astro/client" />
1848
3932
  ` },
1849
3933
  { path: "apps/web/src/pages/index.astro", content: INDEX_ASTRO }
1850
- ],
1851
- owns: [
1852
- "apps/web/package.json",
1853
- "apps/web/astro.config.mjs",
1854
- "apps/web/tsconfig.json",
1855
- "apps/web/src/env.d.ts",
1856
- "apps/web/src/pages/index.astro"
1857
3934
  ]
1858
3935
  };
1859
3936
  });
1860
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
+
1861
3972
  // ../../packages/registry/src/frontends/experimental.ts
1862
3973
  var BABEL_REACT_COMPILER_VERSION = "^1.0.0", experimentalDescriptor = (config) => {
1863
3974
  const { cacheComponents, reactCompiler } = config.experimental;
@@ -1894,7 +4005,7 @@ var BABEL_REACT_COMPILER_VERSION = "^1.0.0", experimentalDescriptor = (config) =
1894
4005
  var init_experimental = () => {};
1895
4006
 
1896
4007
  // ../../packages/registry/src/frontends/next.ts
1897
- var j5 = (value) => `${JSON.stringify(value, null, 2)}
4008
+ var j8 = (value) => `${JSON.stringify(value, null, 2)}
1898
4009
  `, NEXT_CONFIG = `import { createRequire } from "node:module";
1899
4010
  import type { NextConfig } from "next";
1900
4011
 
@@ -1953,7 +4064,7 @@ var init_next = __esm(() => {
1953
4064
  templates: [
1954
4065
  {
1955
4066
  path: "apps/web/package.json",
1956
- content: j5({
4067
+ content: j8({
1957
4068
  name: "web",
1958
4069
  version: "0.0.0",
1959
4070
  private: true,
@@ -1984,7 +4095,7 @@ var init_next = __esm(() => {
1984
4095
  { path: "apps/web/next.config.ts", content: NEXT_CONFIG },
1985
4096
  {
1986
4097
  path: "apps/web/tsconfig.json",
1987
- content: j5({
4098
+ content: j8({
1988
4099
  extends: "@workspace/typescript-config/nextjs.json",
1989
4100
  compilerOptions: { plugins: [{ name: "next" }], paths: { "@/*": ["./*"] } },
1990
4101
  include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
@@ -1994,14 +4105,6 @@ var init_next = __esm(() => {
1994
4105
  { path: "apps/web/app/layout.tsx", content: LAYOUT },
1995
4106
  { path: "apps/web/app/page.tsx", content: PAGE },
1996
4107
  { path: "apps/web/app/globals.css", content: GLOBALS }
1997
- ],
1998
- owns: [
1999
- "apps/web/package.json",
2000
- "apps/web/next.config.ts",
2001
- "apps/web/tsconfig.json",
2002
- "apps/web/app/layout.tsx",
2003
- "apps/web/app/page.tsx",
2004
- "apps/web/app/globals.css"
2005
4108
  ]
2006
4109
  };
2007
4110
  });
@@ -2170,18 +4273,12 @@ export default function AlgoliaSearch({
2170
4273
  { key: "PUBLIC_ALGOLIA_APP_ID", scope: "client" },
2171
4274
  { key: "PUBLIC_ALGOLIA_SEARCH_KEY", scope: "client" },
2172
4275
  { key: "PUBLIC_ALGOLIA_INDEX", scope: "client" }
2173
- ],
2174
- owns: [
2175
- "apps/web/app/search/Search.tsx",
2176
- "apps/web/app/search/page.tsx",
2177
- "apps/web/src/components/AlgoliaSearch.tsx",
2178
- "apps/web/src/pages/search.astro"
2179
4276
  ]
2180
4277
  };
2181
4278
  });
2182
4279
 
2183
4280
  // ../../packages/registry/src/modules/changesets.ts
2184
- var j6 = (v) => `${JSON.stringify(v, null, 2)}
4281
+ var j9 = (v) => `${JSON.stringify(v, null, 2)}
2185
4282
  `, CONFIG, README = `# Changesets
2186
4283
 
2187
4284
  This folder is managed by [Changesets](https://github.com/changesets/changesets).
@@ -2192,7 +4289,7 @@ the \`version-packages\` script consumes them to update versions and changelogs,
2192
4289
  the \`release\` script builds and publishes.
2193
4290
  `, changesets;
2194
4291
  var init_changesets = __esm(() => {
2195
- CONFIG = j6({
4292
+ CONFIG = j9({
2196
4293
  $schema: "https://unpkg.com/@changesets/config@3.1.4/schema.json",
2197
4294
  changelog: "@changesets/cli/changelog",
2198
4295
  commit: false,
@@ -2220,8 +4317,7 @@ var init_changesets = __esm(() => {
2220
4317
  changeset: "changeset",
2221
4318
  "version-packages": "changeset version",
2222
4319
  release: "turbo run build && changeset publish"
2223
- },
2224
- owns: [".changeset/config.json", ".changeset/README.md"]
4320
+ }
2225
4321
  };
2226
4322
  });
2227
4323
 
@@ -2270,8 +4366,7 @@ var init_e2e = __esm(() => {
2270
4366
  ]
2271
4367
  },
2272
4368
  scripts: { e2e: "playwright test" },
2273
- deps: [{ name: "@playwright/test", version: "^1.60.0", target: "root", dev: true }],
2274
- owns: ["tests/e2e/home.spec.ts", "playwright.config.ts"]
4369
+ deps: [{ name: "@playwright/test", version: "^1.60.0", target: "root", dev: true }]
2275
4370
  };
2276
4371
  });
2277
4372
 
@@ -2511,13 +4606,6 @@ export const POST: APIRoute = async ({ request }) => {
2511
4606
  path: "apps/web/astro.config.mjs",
2512
4607
  apply: (content) => ensureAstroAdapter(content, 'import node from "@astrojs/node";', 'node({ mode: "standalone" })')
2513
4608
  }
2514
- ],
2515
- owns: [
2516
- "apps/web/app/contact/page.tsx",
2517
- "apps/web/app/api/contact/route.ts",
2518
- "apps/web/src/components/ContactForm.tsx",
2519
- "apps/web/src/pages/contact.astro",
2520
- "apps/web/src/pages/api/contact.ts"
2521
4609
  ]
2522
4610
  };
2523
4611
  });
@@ -2559,8 +4647,7 @@ var init_git_hooks = __esm(() => {
2559
4647
  target: "root",
2560
4648
  dev: true
2561
4649
  }
2562
- ],
2563
- owns: ["lefthook.yml", "commitlint.config.mjs"]
4650
+ ]
2564
4651
  };
2565
4652
  });
2566
4653
 
@@ -2614,8 +4701,7 @@ var init_github_ci = __esm(() => {
2614
4701
  label: "GitHub Actions CI",
2615
4702
  hint: "Workflow that installs, type-checks, and builds on every push and PR."
2616
4703
  },
2617
- templates: [{ path: ".github/workflows/ci.yml", content: CI_WORKFLOW }],
2618
- owns: [".github/workflows/ci.yml"]
4704
+ templates: [{ path: ".github/workflows/ci.yml", content: CI_WORKFLOW }]
2619
4705
  };
2620
4706
  });
2621
4707
 
@@ -2778,18 +4864,7 @@ ${LLMS_TXT_REST}\`;
2778
4864
  { path: "apps/web/src/content/pages.ts", content: ASTRO_CONTENT },
2779
4865
  { path: "apps/web/src/pages/[slug].md.ts", content: ASTRO_MD_ENDPOINT }
2780
4866
  ]
2781
- },
2782
- owns: [
2783
- "skills.md",
2784
- "apps/web/app/llms.txt/route.ts",
2785
- "apps/web/app/docs/content.ts",
2786
- "apps/web/app/docs/page.tsx",
2787
- "apps/web/app/docs.md/route.ts",
2788
- "apps/web/app/lib/markdown-variant.ts",
2789
- "apps/web/src/pages/llms.txt.ts",
2790
- "apps/web/src/content/pages.ts",
2791
- "apps/web/src/pages/[slug].md.ts"
2792
- ]
4867
+ }
2793
4868
  };
2794
4869
  });
2795
4870
 
@@ -2850,7 +4925,6 @@ var init_posthog = __esm(() => {
2850
4925
  { key: "PUBLIC_POSTHOG_KEY", scope: "client" },
2851
4926
  { key: "PUBLIC_POSTHOG_HOST", scope: "client" }
2852
4927
  ],
2853
- owns: ["apps/web/instrumentation-client.ts", "apps/web/src/components/PostHog.astro"],
2854
4928
  patches: [
2855
4929
  { path: "apps/web/src/pages/index.astro", apply: patchIndexAstro },
2856
4930
  { path: "apps/web/src/layouts/Layout.astro", apply: patchIndexAstro }
@@ -3378,15 +5452,6 @@ var init_sections = __esm(() => {
3378
5452
  { path: "packages/ui/src/components/sections/faq.tsx", content: FAQ_TSX },
3379
5453
  { path: "packages/ui/src/components/sections/cta.tsx", content: CTA_TSX }
3380
5454
  ],
3381
- owns: [
3382
- "packages/ui/src/components/sections/hero.tsx",
3383
- "packages/ui/src/components/sections/features.tsx",
3384
- "packages/ui/src/components/sections/logo-cloud.tsx",
3385
- "packages/ui/src/components/sections/testimonials.tsx",
3386
- "packages/ui/src/components/sections/pricing.tsx",
3387
- "packages/ui/src/components/sections/faq.tsx",
3388
- "packages/ui/src/components/sections/cta.tsx"
3389
- ],
3390
5455
  patches: [
3391
5456
  { path: "apps/web/app/page.tsx", apply: replaceWith(COMPOSED_NEXT_HOME_PAGE) },
3392
5457
  { path: "apps/web/src/pages/index.astro", apply: replaceWith(COMPOSED_ASTRO_HOME_PAGE) }
@@ -3654,14 +5719,6 @@ var init_seo = __esm(() => {
3654
5719
  { key: "PUBLIC_SITE_NAME", scope: "client" },
3655
5720
  { key: "PUBLIC_SITE_DESCRIPTION", scope: "client" }
3656
5721
  ],
3657
- owns: [
3658
- "apps/web/app/lib/metadata.ts",
3659
- "apps/web/app/sitemap.ts",
3660
- "apps/web/app/robots.ts",
3661
- "apps/web/app/opengraph-image.tsx",
3662
- "apps/web/src/components/SEO.astro",
3663
- "apps/web/src/pages/og.png.ts"
3664
- ],
3665
5722
  patches: [
3666
5723
  { path: "apps/web/astro.config.mjs", apply: patchAstroConfig },
3667
5724
  { path: "apps/web/app/layout.tsx", apply: patchLayout },
@@ -3982,7 +6039,7 @@ export function SiteHeader({
3982
6039
  ))}
3983
6040
  </nav>
3984
6041
  <div className="ml-auto">
3985
- <Button size="sm" render={<a href={action.href} />}>
6042
+ <Button size="sm" nativeButton={false} render={<a href={action.href} />}>
3986
6043
  {action.label}
3987
6044
  </Button>
3988
6045
  </div>
@@ -4045,7 +6102,9 @@ export default function NotFound() {
4045
6102
  <p className="text-muted-foreground">
4046
6103
  The link may be out of date, or the page may have moved.
4047
6104
  </p>
4048
- <Button render={<a href="/" />}>Back home</Button>
6105
+ <Button nativeButton={false} render={<a href="/" />}>
6106
+ Back home
6107
+ </Button>
4049
6108
  </main>
4050
6109
  );
4051
6110
  }
@@ -4496,8 +6555,8 @@ ${DARK_TOKENS}
4496
6555
  });
4497
6556
 
4498
6557
  // ../../packages/registry/src/modules/ui/index.ts
4499
- var j7 = (value) => `${JSON.stringify(value, null, 2)}
4500
- `, UI_PACKAGE_JSON, componentsJson = (rsc) => j7({
6558
+ var j10 = (value) => `${JSON.stringify(value, null, 2)}
6559
+ `, UI_PACKAGE_JSON, componentsJson = (rsc) => j10({
4501
6560
  $schema: "https://ui.shadcn.com/schema.json",
4502
6561
  style: "base-nova",
4503
6562
  rsc,
@@ -4520,20 +6579,22 @@ var j7 = (value) => `${JSON.stringify(value, null, 2)}
4520
6579
  menuColor: "default",
4521
6580
  menuAccent: "subtle"
4522
6581
  }), patchNextLayout2 = (content) => {
4523
- if (content.includes("@workspace/ui/components/site-header"))
6582
+ if (content.includes("<SiteHeader"))
4524
6583
  return content;
4525
6584
  const withImports = [
4526
6585
  'import { SiteFooter } from "@workspace/ui/components/site-footer";',
4527
6586
  'import { SiteHeader } from "@workspace/ui/components/site-header";',
4528
- 'import rootPkg from "../../../package.json";',
4529
- 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} />`
4530
6596
  ].join(`
4531
- `);
4532
- return withImports.replace(/<body([^>]*)>\{children\}<\/body>/, `<body$1 className="flex min-h-svh flex-col">
4533
- <SiteHeader siteName={rootPkg.name} />
4534
- <div className="flex-1">{children}</div>
4535
- <SiteFooter siteName={rootPkg.name} />
4536
- </body>`);
6597
+ `));
4537
6598
  }, NEXT_POSTCSS_CONFIG = `export default {
4538
6599
  plugins: {
4539
6600
  "@tailwindcss/postcss": {},
@@ -4544,7 +6605,7 @@ var j7 = (value) => `${JSON.stringify(value, null, 2)}
4544
6605
  var init_ui = __esm(() => {
4545
6606
  init_pages();
4546
6607
  init_theme();
4547
- UI_PACKAGE_JSON = j7({
6608
+ UI_PACKAGE_JSON = j10({
4548
6609
  name: "@workspace/ui",
4549
6610
  version: "0.0.0",
4550
6611
  private: true,
@@ -4608,25 +6669,6 @@ var init_ui = __esm(() => {
4608
6669
  ]
4609
6670
  },
4610
6671
  deps: [{ name: "@workspace/ui", version: "workspace:*", target: "app" }],
4611
- owns: [
4612
- "packages/ui/package.json",
4613
- "packages/ui/components.json",
4614
- "packages/ui/src/styles/globals.css",
4615
- "packages/ui/src/lib/utils.ts",
4616
- "packages/ui/src/components/button.tsx",
4617
- "packages/ui/src/components/card.tsx",
4618
- "packages/ui/src/components/input.tsx",
4619
- "packages/ui/src/components/textarea.tsx",
4620
- "packages/ui/src/components/label.tsx",
4621
- "packages/ui/src/components/badge.tsx",
4622
- "packages/ui/src/components/site-header.tsx",
4623
- "packages/ui/src/components/site-footer.tsx",
4624
- "apps/web/postcss.config.mjs",
4625
- "apps/web/components.json",
4626
- "apps/web/app/not-found.tsx",
4627
- "apps/web/src/layouts/Layout.astro",
4628
- "apps/web/src/pages/404.astro"
4629
- ],
4630
6672
  patches: [
4631
6673
  {
4632
6674
  path: "apps/web/app/globals.css",
@@ -4644,7 +6686,7 @@ var init_ui = __esm(() => {
4644
6686
  if (!plugin || pkg.devDependencies?.[plugin])
4645
6687
  return content;
4646
6688
  const devDependencies = Object.fromEntries(Object.entries({ ...pkg.devDependencies, [plugin]: "^4.3.0" }).sort(([a], [b]) => a.localeCompare(b)));
4647
- return j7({ ...pkg, devDependencies });
6689
+ return j10({ ...pkg, devDependencies });
4648
6690
  }
4649
6691
  },
4650
6692
  { path: "apps/web/app/layout.tsx", apply: patchNextLayout2 },
@@ -4685,8 +6727,7 @@ var init_vitest = __esm(() => {
4685
6727
  { path: "tests/example.test.ts", content: EXAMPLE_TEST }
4686
6728
  ],
4687
6729
  scripts: { test: "vitest run", "test:watch": "vitest" },
4688
- deps: [{ name: "vitest", version: "^4.1.0", target: "root", dev: true }],
4689
- owns: ["vitest.config.ts", "tests/example.test.ts"]
6730
+ deps: [{ name: "vitest", version: "^4.1.0", target: "root", dev: true }]
4690
6731
  };
4691
6732
  });
4692
6733
 
@@ -4694,12 +6735,14 @@ var init_vitest = __esm(() => {
4694
6735
  function buildRegistry(config2) {
4695
6736
  const content = CONTENT_DESCRIPTORS[config2.content];
4696
6737
  const experimental = experimentalDescriptor(config2);
6738
+ const deploy = deployDescriptor(config2);
4697
6739
  return [
4698
6740
  ...baselineDescriptors,
4699
6741
  FRONTEND_DESCRIPTORS[config2.frontend],
4700
6742
  ...experimental ? [experimental] : [],
4701
6743
  ...content ? [content] : [],
4702
- ...config2.modules.map((id) => MODULE_DESCRIPTORS[id])
6744
+ ...config2.modules.map((id) => MODULE_DESCRIPTORS[id]),
6745
+ ...deploy ? [deploy] : []
4703
6746
  ];
4704
6747
  }
4705
6748
  var baselineDescriptors, FRONTEND_DESCRIPTORS, MODULE_DESCRIPTORS, CONTENT_DESCRIPTORS;
@@ -4711,6 +6754,7 @@ var init_src3 = __esm(() => {
4711
6754
  init_markdown();
4712
6755
  init_sanity();
4713
6756
  init_astro();
6757
+ init_deploy_target();
4714
6758
  init_experimental();
4715
6759
  init_next();
4716
6760
  init_algolia();
@@ -4790,72 +6834,6 @@ var init_create = __esm(() => {
4790
6834
  init_src3();
4791
6835
  });
4792
6836
 
4793
- // package.json
4794
- var package_default;
4795
- var init_package = __esm(() => {
4796
- package_default = {
4797
- name: "create-turbo-wizard",
4798
- version: "0.1.0",
4799
- description: "Scaffold a Turborepo monorepo: Astro or Next.js, Sanity or Markdown, opt-in modules.",
4800
- keywords: [
4801
- "turborepo",
4802
- "monorepo",
4803
- "scaffold",
4804
- "astro",
4805
- "nextjs",
4806
- "sanity",
4807
- "shadcn",
4808
- "tailwind",
4809
- "marketing-site",
4810
- "create"
4811
- ],
4812
- homepage: "https://turbo-wizard.vercel.app",
4813
- repository: {
4814
- type: "git",
4815
- url: "git+https://github.com/robotostudio/turbo-wizard.git",
4816
- directory: "apps/cli"
4817
- },
4818
- bugs: {
4819
- url: "https://github.com/robotostudio/turbo-wizard/issues"
4820
- },
4821
- author: "Roboto Studio",
4822
- license: "MIT",
4823
- type: "module",
4824
- bin: {
4825
- "create-turbo-wizard": "dist/cli.js"
4826
- },
4827
- files: [
4828
- "dist",
4829
- "README.md"
4830
- ],
4831
- scripts: {
4832
- "check-types": "tsc --noEmit",
4833
- test: "bun test",
4834
- start: "bun run src/cli.ts",
4835
- build: "bun run scripts/bundle.ts",
4836
- prepublishOnly: "bun run scripts/bundle.ts"
4837
- },
4838
- dependencies: {
4839
- "@clack/prompts": "^1.5.0",
4840
- "@modelcontextprotocol/sdk": "^1.29.0",
4841
- "@trpc/server": "^11.0.0",
4842
- "trpc-cli": "^0.14.1",
4843
- zod: "^4.4.3"
4844
- },
4845
- devDependencies: {
4846
- "@turbo-wizard/engine": "workspace:*",
4847
- "@turbo-wizard/registry": "workspace:*",
4848
- "@turbo-wizard/types": "workspace:*"
4849
- },
4850
- engines: {
4851
- node: ">=22"
4852
- },
4853
- publishConfig: {
4854
- access: "public"
4855
- }
4856
- };
4857
- });
4858
-
4859
6837
  // src/mcp.ts
4860
6838
  var exports_mcp = {};
4861
6839
  __export(exports_mcp, {
@@ -4941,6 +6919,7 @@ var init_mcp = __esm(() => {
4941
6919
 
4942
6920
  // src/router.ts
4943
6921
  init_src();
6922
+ init_package();
4944
6923
  init_create();
4945
6924
  import { initTRPC } from "@trpc/server";
4946
6925
  import { createCli } from "trpc-cli";
@@ -5002,6 +6981,7 @@ var CreateInput = z2.object({
5002
6981
  frontend: z2.enum(FRONTENDS).optional(),
5003
6982
  content: z2.enum(CONTENT_SOURCES).optional(),
5004
6983
  packageManager: z2.enum(PACKAGE_MANAGERS).optional(),
6984
+ deploy: z2.enum(DEPLOY_TARGETS).optional(),
5005
6985
  modules: z2.array(z2.enum(MODULES)).optional(),
5006
6986
  preset: z2.enum(PRESETS).optional(),
5007
6987
  cacheComponents: z2.boolean().optional(),
@@ -5012,13 +6992,14 @@ var CreateInput = z2.object({
5012
6992
  yes: z2.boolean().default(false)
5013
6993
  });
5014
6994
  var router = t.router({
5015
- create: t.procedure.input(CreateInput).mutation(async ({ input }) => {
6995
+ create: t.procedure.meta({ default: true }).input(CreateInput).mutation(async ({ input }) => {
5016
6996
  const base = input.preset ? applyPreset(input.preset) : {};
5017
6997
  const flags = {
5018
6998
  name: input.name,
5019
6999
  frontend: input.frontend,
5020
7000
  content: input.content ?? base.content,
5021
7001
  packageManager: input.packageManager ?? base.packageManager,
7002
+ deploy: input.deploy ?? base.deploy,
5022
7003
  modules: input.modules ?? base.modules,
5023
7004
  cacheComponents: input.cacheComponents ?? base.experimental?.cacheComponents,
5024
7005
  reactCompiler: input.reactCompiler ?? base.experimental?.reactCompiler
@@ -5029,6 +7010,7 @@ var router = t.router({
5029
7010
  frontend: flags.frontend ?? "next",
5030
7011
  content: flags.content,
5031
7012
  packageManager: flags.packageManager,
7013
+ deploy: flags.deploy,
5032
7014
  modules: flags.modules,
5033
7015
  experimental: {
5034
7016
  cacheComponents: flags.cacheComponents,
@@ -5048,7 +7030,7 @@ var router = t.router({
5048
7030
  console.error(` next: cd ${res.value.dir} && ${config2.packageManager} install`);
5049
7031
  })
5050
7032
  });
5051
- var cli = createCli({ router, name: "create-turbo-wizard", version: "0.0.0" });
7033
+ var cli = createCli({ router, name: "create-turbo-wizard", version: package_default.version });
5052
7034
 
5053
7035
  // src/cli.ts
5054
7036
  if (process.argv[2] === "mcp") {