create-turbo-wizard 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +48 -15
  2. package/dist/cli.js +3089 -789
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -15,13 +15,38 @@ var __export = (target, all) => {
15
15
  };
16
16
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
17
 
18
+ // ../../packages/types/src/command.ts
19
+ function buildCommand(config) {
20
+ const parts = [
21
+ ...CREATE_PREFIX[config.packageManager],
22
+ `--name ${config.name}`,
23
+ `--frontend ${config.frontend}`,
24
+ `--content ${config.content}`,
25
+ `--package-manager ${config.packageManager}`
26
+ ];
27
+ config.deploy !== "node" && parts.push(`--deploy ${config.deploy}`);
28
+ config.modules.length && parts.push(`--modules ${config.modules.join(" ")}`);
29
+ config.experimental.cacheComponents && parts.push("--cache-components");
30
+ config.experimental.reactCompiler && parts.push("--react-compiler");
31
+ return parts.join(" ");
32
+ }
33
+ var CREATE_PREFIX;
34
+ var init_command = __esm(() => {
35
+ CREATE_PREFIX = {
36
+ npm: ["npm create turbo-wizard@latest", "--"],
37
+ pnpm: ["pnpm create turbo-wizard@latest"],
38
+ bun: ["bun create turbo-wizard@latest"]
39
+ };
40
+ });
41
+
18
42
  // ../../packages/types/src/index.ts
19
43
  import { z } from "zod";
20
44
  function applyPreset(preset, overrides = {}) {
21
45
  return { ...PRESET_CONFIGS[preset], ...overrides };
22
46
  }
23
- var FRONTENDS, CONTENT_SOURCES, PACKAGE_MANAGERS, MODULES, FrontendSchema, ContentSourceSchema, PackageManagerSchema, ModuleSchema, ExperimentalSchema, ProjectNameSchema, WizardConfigSchema, wizardConfigJsonSchema = () => z.toJSONSchema(WizardConfigSchema), PRESETS, PRESET_CONFIGS;
47
+ var FRONTENDS, CONTENT_SOURCES, PACKAGE_MANAGERS, MODULES, MODULE_CATEGORIES, DEPLOY_TARGETS, FrontendSchema, ContentSourceSchema, PackageManagerSchema, ModuleSchema, DeployTargetSchema, ExperimentalSchema, ProjectNameSchema, WizardConfigSchema, wizardConfigJsonSchema = () => z.toJSONSchema(WizardConfigSchema), PRESETS, PRESET_CONFIGS;
24
48
  var init_src = __esm(() => {
49
+ init_command();
25
50
  FRONTENDS = ["astro", "next"];
26
51
  CONTENT_SOURCES = ["sanity", "markdown", "none"];
27
52
  PACKAGE_MANAGERS = ["pnpm", "bun", "npm"];
@@ -39,10 +64,13 @@ var init_src = __esm(() => {
39
64
  "forms",
40
65
  "llms"
41
66
  ];
67
+ MODULE_CATEGORIES = ["devtools", "integration", "ui", "cross-cutting"];
68
+ DEPLOY_TARGETS = ["node", "vercel"];
42
69
  FrontendSchema = z.enum(FRONTENDS).describe("Web framework");
43
70
  ContentSourceSchema = z.enum(CONTENT_SOURCES).describe("Content source");
44
71
  PackageManagerSchema = z.enum(PACKAGE_MANAGERS).describe("Package manager");
45
72
  ModuleSchema = z.enum(MODULES).describe("Opt-in module");
73
+ DeployTargetSchema = z.enum(DEPLOY_TARGETS).describe("Deploy target (Astro SSR adapter; ignored by Next)");
46
74
  ExperimentalSchema = z.object({
47
75
  cacheComponents: z.boolean().default(false),
48
76
  reactCompiler: z.boolean().default(false)
@@ -53,6 +81,7 @@ var init_src = __esm(() => {
53
81
  frontend: FrontendSchema,
54
82
  content: ContentSourceSchema.default("none"),
55
83
  packageManager: PackageManagerSchema.default("pnpm"),
84
+ deploy: DeployTargetSchema.default("node"),
56
85
  modules: z.array(ModuleSchema).default([]),
57
86
  experimental: ExperimentalSchema
58
87
  });
@@ -72,7 +101,7 @@ var package_default;
72
101
  var init_package = __esm(() => {
73
102
  package_default = {
74
103
  name: "create-turbo-wizard",
75
- version: "0.1.1",
104
+ version: "0.2.0",
76
105
  description: "Scaffold a Turborepo monorepo: Astro or Next.js, Sanity or Markdown, opt-in modules.",
77
106
  keywords: [
78
107
  "turborepo",
@@ -289,6 +318,7 @@ ${WORKSPACE_GLOBS.map((g) => ` - "${g}"`).join(`
289
318
  "dist/",
290
319
  ".next/",
291
320
  ".astro/",
321
+ ".sanity/",
292
322
  ".turbo/",
293
323
  "*.tsbuildinfo",
294
324
  "",
@@ -380,8 +410,7 @@ var init_biome = __esm(() => {
380
410
  kind: "baseline",
381
411
  templates: [{ path: "biome.jsonc", content: BIOME_CONFIG }],
382
412
  scripts: { check: "biome check .", format: "biome format --write ." },
383
- deps: [{ name: "@biomejs/biome", versionKey: "@biomejs/biome", target: "root", dev: true }],
384
- owns: ["biome.jsonc"]
413
+ deps: [{ name: "@biomejs/biome", versionKey: "@biomejs/biome", target: "root", dev: true }]
385
414
  };
386
415
  });
387
416
 
@@ -426,8 +455,7 @@ var init_env = __esm(() => {
426
455
  content: j({ extends: "@workspace/typescript-config/base.json", include: ["src"] })
427
456
  },
428
457
  { path: "packages/env/src/index.ts", content: ENV_SRC }
429
- ],
430
- owns: ["packages/env/package.json", "packages/env/tsconfig.json", "packages/env/src/index.ts"]
458
+ ]
431
459
  };
432
460
  });
433
461
 
@@ -475,11 +503,6 @@ var init_logger = __esm(() => {
475
503
  content: j2({ extends: "@workspace/typescript-config/base.json", include: ["src"] })
476
504
  },
477
505
  { path: "packages/logger/src/index.ts", content: LOGGER_SRC }
478
- ],
479
- owns: [
480
- "packages/logger/package.json",
481
- "packages/logger/tsconfig.json",
482
- "packages/logger/src/index.ts"
483
506
  ]
484
507
  };
485
508
  });
@@ -543,12 +566,6 @@ var init_typescript_config = __esm(() => {
543
566
  compilerOptions: { lib: ["DOM", "DOM.Iterable", "ES2023"], jsx: "react-jsx" }
544
567
  })
545
568
  }
546
- ],
547
- owns: [
548
- "packages/typescript-config/package.json",
549
- "packages/typescript-config/base.json",
550
- "packages/typescript-config/nextjs.json",
551
- "packages/typescript-config/react-library.json"
552
569
  ]
553
570
  };
554
571
  });
@@ -616,7 +633,8 @@ function replaceNextMetadata(content, expr) {
616
633
  }
617
634
 
618
635
  // ../../packages/registry/src/content/markdown.ts
619
- var NEXT_MDX_COMPONENTS = `import type { MDXComponents } from "mdx/types";
636
+ var j4 = (value) => `${JSON.stringify(value, null, 2)}
637
+ `, CONTENT_PACKAGE_JSON, NEXT_MDX_COMPONENTS = `import type { MDXComponents } from "mdx/types";
620
638
 
621
639
  const components: MDXComponents = {
622
640
  // Override built-ins here, e.g. h1: ({ children }) => <h1 className="...">{children}</h1>,
@@ -642,15 +660,26 @@ This post is written in **MDX** — Markdown with JSX.
642
660
  - Mix Markdown prose with interactive components
643
661
  - Full JSX expression support
644
662
  - Server Components by default
663
+ `, NEXT_CONTENT_SCHEMA = `export interface PostMetadata {
664
+ title: string;
665
+ description?: string;
666
+ date?: string;
667
+ }
645
668
  `, NEXT_BLOG_POSTS_TS = `import { readdir } from "node:fs/promises";
646
669
  import path from "node:path";
670
+ import type { PostMetadata } from "@workspace/content/schema";
647
671
  import type { ComponentType } from "react";
648
672
 
649
- const POSTS_DIR = path.join(process.cwd(), "content/blog");
673
+ // cwd is apps/web for every next command turbo runs; the posts are two levels up in the
674
+ // workspace. In the default configuration this is read only at build time — the listing
675
+ // prerenders and generateStaticParams enumerates the slugs. With cacheComponents on, the
676
+ // PostList boundary below streams at request time and reads it then, exactly as it did
677
+ // when the posts lived in apps/web/content.
678
+ const POSTS_DIR = path.join(process.cwd(), "../../packages/content/blog");
650
679
 
651
680
  export interface PostModule {
652
681
  default: ComponentType;
653
- metadata: { title: string; description?: string; date?: string };
682
+ metadata: PostMetadata;
654
683
  }
655
684
 
656
685
  export async function getPostSlugs(): Promise<string[]> {
@@ -665,7 +694,7 @@ async function getPosts() {
665
694
  const slugs = await getPostSlugs();
666
695
  return Promise.all(
667
696
  slugs.map(async (slug) => {
668
- const { metadata } = (await import(\`@/content/blog/\${slug}.mdx\`)) as PostModule;
697
+ const { metadata } = (await import(\`@workspace/content/blog/\${slug}.mdx\`)) as PostModule;
669
698
  return { slug, ...metadata };
670
699
  }),
671
700
  );
@@ -719,7 +748,7 @@ export default async function BlogPostPage({
719
748
  params: Promise<{ slug: string }>;
720
749
  }) {
721
750
  const { slug } = await params;
722
- const post = (await import(\`@/content/blog/\${slug}.mdx\`).catch(() => null)) as PostModule | null;
751
+ const post = (await import(\`@workspace/content/blog/\${slug}.mdx\`).catch(() => null)) as PostModule | null;
723
752
  if (!post) notFound();
724
753
 
725
754
  const { default: Post, metadata } = post;
@@ -733,18 +762,23 @@ export default async function BlogPostPage({
733
762
  </main>
734
763
  );
735
764
  }
736
- `, ASTRO_CONTENT_CONFIG = `import { glob } from "astro/loaders";
737
- import { defineCollection, z } from "astro:content";
765
+ `, ASTRO_CONTENT_SCHEMA = `import { z } from "astro:content";
766
+
767
+ export const blogSchema = z.object({
768
+ title: z.string(),
769
+ description: z.string(),
770
+ pubDate: z.coerce.date(),
771
+ updatedDate: z.coerce.date().optional(),
772
+ });
773
+ `, ASTRO_CONTENT_CONFIG = `import { blogSchema } from "@workspace/content/schema";
774
+ import { glob } from "astro/loaders";
775
+ import { defineCollection } from "astro:content";
738
776
 
739
777
  const blog = defineCollection({
740
- // Pattern includes both .md and .mdx so the collection is mixed-format.
741
- loader: glob({ pattern: "**/*.{md,mdx}", base: "./src/content/blog" }),
742
- schema: z.object({
743
- title: z.string(),
744
- description: z.string(),
745
- pubDate: z.coerce.date(),
746
- updatedDate: z.coerce.date().optional(),
747
- }),
778
+ // Pattern includes both .md and .mdx so the collection is mixed-format. \`base\` is
779
+ // resolved from the project root (apps/web), so it reaches out to the package.
780
+ loader: glob({ pattern: "**/*.{md,mdx}", base: "../../packages/content/blog" }),
781
+ schema: blogSchema,
748
782
  });
749
783
 
750
784
  export const collections = { blog };
@@ -822,6 +856,16 @@ const { Content } = await render(post);
822
856
  </html>
823
857
  `, markdown;
824
858
  var init_markdown = __esm(() => {
859
+ CONTENT_PACKAGE_JSON = j4({
860
+ name: "@workspace/content",
861
+ version: "0.0.0",
862
+ private: true,
863
+ type: "module",
864
+ exports: {
865
+ "./schema": "./src/schema.ts",
866
+ "./blog/*": "./blog/*"
867
+ }
868
+ });
825
869
  markdown = {
826
870
  id: "markdown",
827
871
  kind: "content",
@@ -829,21 +873,25 @@ var init_markdown = __esm(() => {
829
873
  label: "MDX content",
830
874
  hint: "Markdown + JSX wired into the frontend (official recipe)"
831
875
  },
876
+ templates: [{ path: "packages/content/package.json", content: CONTENT_PACKAGE_JSON }],
832
877
  fragments: {
833
878
  next: [
879
+ { path: "packages/content/src/schema.ts", content: NEXT_CONTENT_SCHEMA },
880
+ { path: "packages/content/blog/hello.mdx", content: NEXT_BLOG_POST_MDX },
834
881
  { path: "apps/web/mdx-components.tsx", content: NEXT_MDX_COMPONENTS },
835
- { path: "apps/web/content/blog/hello.mdx", content: NEXT_BLOG_POST_MDX },
836
882
  { path: "apps/web/app/blog/posts.ts", content: NEXT_BLOG_POSTS_TS },
837
883
  { path: "apps/web/app/blog/page.tsx", content: NEXT_BLOG_INDEX_TSX },
838
884
  { path: "apps/web/app/blog/[slug]/page.tsx", content: NEXT_BLOG_SLUG_TSX }
839
885
  ],
840
886
  astro: [
887
+ { path: "packages/content/src/schema.ts", content: ASTRO_CONTENT_SCHEMA },
888
+ { path: "packages/content/blog/hello.mdx", content: ASTRO_BLOG_POST },
841
889
  { path: "apps/web/src/content.config.ts", content: ASTRO_CONTENT_CONFIG },
842
- { path: "apps/web/src/content/blog/hello.mdx", content: ASTRO_BLOG_POST },
843
890
  { path: "apps/web/src/pages/blog/index.astro", content: ASTRO_BLOG_INDEX },
844
891
  { path: "apps/web/src/pages/blog/[...slug].astro", content: ASTRO_BLOG_SLUG_PAGE }
845
892
  ]
846
893
  },
894
+ deps: [{ name: "@workspace/content", version: "workspace:*", target: "app" }],
847
895
  frontendDeps: {
848
896
  next: [
849
897
  { name: "@next/mdx", version: "^16.2.0", target: "app", dev: true },
@@ -853,17 +901,6 @@ var init_markdown = __esm(() => {
853
901
  ],
854
902
  astro: [{ name: "@astrojs/mdx", version: "^4.3.14", target: "app" }]
855
903
  },
856
- owns: [
857
- "apps/web/mdx-components.tsx",
858
- "apps/web/content/blog/hello.mdx",
859
- "apps/web/app/blog/posts.ts",
860
- "apps/web/app/blog/page.tsx",
861
- "apps/web/app/blog/[slug]/page.tsx",
862
- "apps/web/src/content.config.ts",
863
- "apps/web/src/content/blog/hello.mdx",
864
- "apps/web/src/pages/blog/index.astro",
865
- "apps/web/src/pages/blog/[...slug].astro"
866
- ],
867
904
  patches: [
868
905
  {
869
906
  path: "apps/web/next.config.ts",
@@ -879,219 +916,393 @@ var init_markdown = __esm(() => {
879
916
  };
880
917
  });
881
918
 
882
- // ../../packages/registry/src/content/sanity.ts
883
- var SCHEMA_BLOCKS = `import { defineArrayMember, defineField, defineType } from "sanity";
884
-
885
- // Page-builder blocks are objects, not documents: they only exist inside a page.
886
-
887
- export const hero = defineType({
888
- name: "hero",
889
- title: "Hero",
890
- type: "object",
891
- fields: [
892
- defineField({ name: "heading", type: "string", validation: (rule) => rule.required() }),
893
- defineField({ name: "subheading", type: "text", rows: 2 }),
894
- defineField({
895
- name: "image",
896
- type: "image",
897
- options: { hotspot: true },
898
- fields: [defineField({ name: "alt", type: "string", title: "Alternative text" })],
899
- }),
900
- ],
901
- preview: {
902
- select: { title: "heading", media: "image" },
903
- prepare: ({ title, media }) => ({ title: title ?? "Hero", subtitle: "Hero", media }),
904
- },
905
- });
906
-
907
- export const richText = defineType({
908
- name: "richText",
909
- title: "Rich text",
910
- type: "object",
911
- fields: [
912
- defineField({ name: "heading", type: "string" }),
913
- defineField({ name: "content", type: "array", of: [defineArrayMember({ type: "block" })] }),
914
- ],
915
- preview: {
916
- select: { title: "heading" },
917
- prepare: ({ title }) => ({ title: title ?? "Rich text", subtitle: "Rich text" }),
918
- },
919
- });
919
+ // ../../packages/registry/src/content/sanity/astro.ts
920
+ var ASTRO_ENV = `export const projectId = import.meta.env.PUBLIC_SANITY_PROJECT_ID || "placeholder";
920
921
 
921
- export const cta = defineType({
922
- name: "cta",
923
- title: "Call to action",
924
- type: "object",
925
- fields: [
926
- defineField({ name: "heading", type: "string", validation: (rule) => rule.required() }),
927
- defineField({ name: "text", type: "text", rows: 2 }),
928
- defineField({ name: "buttonLabel", type: "string" }),
929
- defineField({ name: "buttonHref", type: "string" }),
930
- ],
931
- preview: {
932
- select: { title: "heading" },
933
- prepare: ({ title }) => ({ title: title ?? "Call to action", subtitle: "Call to action" }),
934
- },
935
- });
936
- `, SCHEMA_PAGE = `import { defineArrayMember, defineField, defineType } from "sanity";
922
+ export const dataset = import.meta.env.PUBLIC_SANITY_DATASET || "production";
937
923
 
938
- export const page = defineType({
939
- name: "page",
940
- title: "Page",
941
- type: "document",
942
- fields: [
943
- defineField({ name: "title", type: "string", validation: (rule) => rule.required() }),
944
- defineField({
945
- name: "slug",
946
- type: "slug",
947
- options: { source: "title" },
948
- validation: (rule) => rule.required(),
949
- }),
950
- defineField({ name: "description", type: "text", rows: 3 }),
951
- defineField({
952
- name: "pageBuilder",
953
- title: "Page builder",
954
- type: "array",
955
- of: [
956
- defineArrayMember({ type: "hero" }),
957
- defineArrayMember({ type: "richText" }),
958
- defineArrayMember({ type: "cta" }),
959
- ],
960
- }),
961
- ],
962
- // The home page is the \`page\` whose slug is "home" — see sanity/queries.ts.
963
- preview: { select: { title: "title", subtitle: "slug.current" } },
964
- });
965
- `, SCHEMA_SETTINGS = `import { defineField, defineType } from "sanity";
924
+ /** Server-only read token; required (and only read) when Visual Editing is enabled. */
925
+ export const readToken = import.meta.env.SANITY_API_READ_TOKEN ?? "";
966
926
 
967
- export const settings = defineType({
968
- name: "settings",
969
- title: "Settings",
970
- type: "document",
971
- fields: [
972
- defineField({ name: "siteTitle", type: "string", validation: (rule) => rule.required() }),
973
- defineField({ name: "siteDescription", type: "text", rows: 3 }),
974
- defineField({
975
- name: "socialImage",
976
- title: "Social share image",
977
- type: "image",
978
- options: { hotspot: true },
979
- }),
980
- ],
981
- preview: { select: { title: "siteTitle", media: "socialImage" } },
982
- });
983
- `, SCHEMA_INDEX = `import type { SchemaTypeDefinition } from "sanity";
984
- import { cta, hero, richText } from "./blocks";
985
- import { page } from "./page";
986
- import { settings } from "./settings";
927
+ /**
928
+ * The @sanity/astro Visual Editing switch. Off by default: overlays need a rendering
929
+ * server (\`astro dev\`, or an SSR adapter in production) plus SANITY_API_READ_TOKEN.
930
+ */
931
+ export const visualEditingEnabled =
932
+ import.meta.env.PUBLIC_SANITY_VISUAL_EDITING_ENABLED === "true";
987
933
 
988
- export const schemaTypes: SchemaTypeDefinition[] = [page, settings, hero, richText, cta];
989
- `, STRUCTURE = `import type { StructureResolver } from "sanity/structure";
934
+ /**
935
+ * False on a freshly scaffolded project. Guards every fetch so \`astro build\` never dials
936
+ * placeholder.api.sanity.io and dies; pages render an empty state instead.
937
+ */
938
+ export const hasSanityCredentials = projectId !== "placeholder";
939
+ `, ASTRO_FETCH = `import { sanityClient } from "sanity:client";
940
+ import { hasSanityCredentials, readToken, visualEditingEnabled } from "./env";
990
941
 
991
942
  /**
992
- * \`settings\` is a singleton: one document with a fixed id, edited in place instead of
993
- * listed. Everything else falls through to the default document-type lists.
943
+ * \`sanity:client\` is a Vite virtual module and this file lives in a linked workspace
944
+ * package, not in apps/web. Both halves of that were measured before the move: the
945
+ * integration's \`resolveId\` fires for any importer in the graph, and Vite still applies
946
+ * its \`import.meta.env\` substitution inside the package (a pnpm workspace link resolves
947
+ * to a real path outside node_modules, so it is treated as source, not as an external
948
+ * dependency). Without the second half every read would silently short-circuit on a
949
+ * "placeholder" project id and every route would still answer 200.
994
950
  *
995
- * The two sets feed sanity.config.ts's \`document.newDocumentOptions\` / \`document.actions\`
996
- * guards (the official singleton recipe), so a second settings document cannot be created.
951
+ * The single read path, following @sanity/astro's Visual Editing recipe: with
952
+ * PUBLIC_SANITY_VISUAL_EDITING_ENABLED=true it fetches the drafts perspective with
953
+ * stega source maps, which is what gives Presentation its click-to-edit overlays.
954
+ *
955
+ * Without credentials it returns null before touching the network, so \`astro build\`
956
+ * succeeds on a fresh scaffold. The catch covers a configured-but-unreachable dataset
957
+ * for the same reason: a starter should render empty, not crash.
997
958
  */
998
- export const singletonTypes = new Set(["settings"]);
999
-
1000
- /** Actions that keep a singleton editable without allowing create/delete/duplicate. */
1001
- export const singletonActions = new Set(["publish", "discardChanges", "restore"]);
1002
-
1003
- export const structure: StructureResolver = (S) =>
1004
- S.list()
1005
- .title("Content")
1006
- .items([
1007
- S.listItem()
1008
- .title("Settings")
1009
- .id("settings")
1010
- .child(S.document().schemaType("settings").documentId("settings")),
1011
- S.divider(),
1012
- S.documentTypeListItem("page").title("Pages"),
1013
- ]);
1014
- `, PRESENTATION = `import { defineDocuments, defineLocations } from "sanity/presentation";
1015
-
1016
- /** Route → document: lets Presentation open the right \`page\` for the URL in the iframe. */
1017
- export const mainDocuments = defineDocuments([
1018
- { route: "/", filter: \`_type == "page" && slug.current == "home"\` },
1019
- { route: "/:slug", filter: \`_type == "page" && slug.current == $slug\` },
1020
- ]);
1021
-
1022
- /** Document → routes: the "Used on …" links shown when editing a document. */
1023
- export const locations = {
1024
- settings: defineLocations({
1025
- locations: [{ title: "Home", href: "/" }],
1026
- message: "This document is used on all pages",
1027
- tone: "positive",
1028
- }),
1029
- page: defineLocations({
1030
- select: { title: "title", slug: "slug.current" },
1031
- resolve: (doc) => ({
1032
- locations: [
1033
- {
1034
- title: doc?.title || "Untitled",
1035
- href: doc?.slug === "home" ? "/" : \`/\${doc?.slug}\`,
1036
- },
1037
- ],
1038
- }),
1039
- }),
1040
- };
1041
- `, DOCUMENT_GUARD = ` document: {
1042
- newDocumentOptions: (prev, { creationContext }) =>
1043
- creationContext.type === "global"
1044
- ? prev.filter((template) => !singletonTypes.has(template.templateId))
1045
- : prev,
1046
- actions: (prev, { schemaType }) =>
1047
- singletonTypes.has(schemaType)
1048
- ? prev.filter(({ action }) => action && singletonActions.has(action))
1049
- : prev,
1050
- },`, QUERIES = `import { defineQuery } from "groq";
1051
-
1052
- const PAGE_FIELDS = /* groq */ \`
1053
- _id,
1054
- _type,
1055
- title,
1056
- description,
1057
- "slug": slug.current,
1058
- pageBuilder[]{ ... }
1059
- \`;
1060
-
1061
- export const HOME_PAGE_QUERY = defineQuery(
1062
- \`*[_type == "page" && slug.current == "home"][0]{\${PAGE_FIELDS}}\`,
1063
- );
959
+ export async function loadQuery<T>(
960
+ query: string,
961
+ params: Record<string, unknown> = {},
962
+ ): Promise<T | null> {
963
+ if (!hasSanityCredentials) return null;
964
+ if (visualEditingEnabled && !readToken) {
965
+ throw new Error(
966
+ "SANITY_API_READ_TOKEN is required when PUBLIC_SANITY_VISUAL_EDITING_ENABLED is true.",
967
+ );
968
+ }
969
+ try {
970
+ const { result } = await sanityClient.fetch<T>(query, params, {
971
+ filterResponse: false,
972
+ perspective: visualEditingEnabled ? "drafts" : "published",
973
+ resultSourceMap: visualEditingEnabled ? "withKeyArraySelector" : false,
974
+ stega: visualEditingEnabled,
975
+ ...(visualEditingEnabled ? { token: readToken } : {}),
976
+ useCdn: !visualEditingEnabled,
977
+ });
978
+ return result;
979
+ } catch (error) {
980
+ console.warn("[sanity] query failed:", error);
981
+ return null;
982
+ }
983
+ }
984
+ `, ASTRO_IMAGE = `import { createImageUrlBuilder } from "@sanity/image-url";
985
+ import { dataset, projectId } from "./env";
1064
986
 
1065
- export const PAGE_QUERY = defineQuery(
1066
- \`*[_type == "page" && slug.current == $slug][0]{\${PAGE_FIELDS}}\`,
1067
- );
987
+ const builder = createImageUrlBuilder({ projectId, dataset });
1068
988
 
1069
- export const PAGE_SLUGS_QUERY = defineQuery(
1070
- \`*[_type == "page" && defined(slug.current) && slug.current != "home"].slug.current\`,
1071
- );
989
+ /** Build a CDN URL for a Sanity image; null when the block has no asset. */
990
+ export function urlFor(source: unknown, width = 1200): string | null {
991
+ const ref = (source as { asset?: { _ref?: string } } | null)?.asset?._ref;
992
+ if (!ref) return null;
993
+ return builder.image(ref).width(width).auto("format").url();
994
+ }
995
+ `, ASTRO_PAGE_BUILDER = `---
996
+ import { urlFor } from "@workspace/sanity/image";
997
+ import type { PAGE_QUERY_RESULT } from "@workspace/sanity/types";
998
+ import { PortableText } from "astro-portabletext";
1072
999
 
1073
- export const SETTINGS_QUERY = defineQuery(
1074
- \`*[_type == "settings"][0]{ siteTitle, siteDescription, socialImage }\`,
1075
- );
1076
- `, SEED_NDJSON = `{"_id":"settings","_type":"settings","siteTitle":"Turbo Wizard","siteDescription":"A Turborepo monorepo scaffolded with create-turbo-wizard, with Sanity wired end to end."}
1077
- {"_id":"page-home","_type":"page","title":"Turbo Wizard","description":"The home page, served from Sanity.","slug":{"_type":"slug","current":"home"},"pageBuilder":[{"_key":"hero1","_type":"hero","heading":"Content from Sanity, rendered by your app","subheading":"This page lives in the Studio. Edit it there and watch it change here."},{"_key":"rich1","_type":"richText","heading":"How this fits together","content":[{"_key":"b1","_type":"block","style":"normal","markDefs":[],"children":[{"_key":"s1","_type":"span","marks":[],"text":"The schema lives in apps/web/sanity/schema. Queries live in apps/web/sanity/queries.ts and run through typegen, so results are typed. The page builder renders each block by its _type."}]}]},{"_key":"cta1","_type":"cta","heading":"Open the Studio","text":"Edit this page and reload.","buttonLabel":"Open Studio","buttonHref":"/studio"}]}
1078
- {"_id":"page-about","_type":"page","title":"About","description":"A second page, to show routing by slug.","slug":{"_type":"slug","current":"about"},"pageBuilder":[{"_key":"hero2","_type":"hero","heading":"About this starter","subheading":"Pages are routed by their slug."},{"_key":"rich2","_type":"richText","content":[{"_key":"b2","_type":"block","style":"normal","markDefs":[],"children":[{"_key":"s2","_type":"span","marks":[],"text":"Add a page in the Studio, give it a slug, and it appears at that path."}]}]}]}
1079
- `, NEXT_ENV = `export const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID || "placeholder";
1000
+ // Derived from the generated query result rather than hand-written: the page builder's
1001
+ // shape IS what the projection returns, so typegen is the only source that cannot drift.
1002
+ type PageBlock = NonNullable<NonNullable<PAGE_QUERY_RESULT>["pageBuilder"]>[number];
1003
+ type ButtonLink = NonNullable<Extract<PageBlock, { _type: "hero" }>["buttons"]>[number];
1080
1004
 
1081
- export const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET || "production";
1005
+ interface Props {
1006
+ blocks: PageBlock[];
1007
+ }
1082
1008
 
1083
- // A recent pinned date, per Sanity's API-versioning guidance (the docs pin the date the
1084
- // project was set up; bump deliberately, never to "today").
1085
- export const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION || "2026-02-01";
1009
+ const { blocks } = Astro.props;
1086
1010
 
1087
- /** Server-only read token; empty when unset (draft mode and live previews need it). */
1088
- export const readToken = process.env.SANITY_API_READ_TOKEN ?? "";
1011
+ const section = "margin: 3rem 0;";
1012
+ const grid = "display: grid; gap: 1.5rem; grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));";
1013
+ const card = "border: 1px solid #e5e5e5; border-radius: 0.75rem; padding: 1.25rem;";
1089
1014
 
1090
- /** The embedded Studio's route — stega encodes it into every string it returns. */
1091
- export const studioUrl = "/studio";
1015
+ const buttonProps = (button: ButtonLink) =>
1016
+ button.openInNewTab ? { target: "_blank", rel: "noreferrer" } : {};
1017
+ ---
1092
1018
 
1093
- /**
1094
- * False on a freshly scaffolded project. Guards every fetch so the build never dials
1019
+ {
1020
+ blocks.map((block) => {
1021
+ if (block._type === "hero") {
1022
+ const src = urlFor(block.image, 1600);
1023
+ return (
1024
+ <section style={section}>
1025
+ {block.badge && <p style="opacity: 0.6; margin: 0;">{block.badge}</p>}
1026
+ <h2>{block.title}</h2>
1027
+ {block.richText && <PortableText value={block.richText} />}
1028
+ {!!block.buttons?.length && (
1029
+ <p style="display: flex; gap: 0.75rem; flex-wrap: wrap;">
1030
+ {block.buttons.map(
1031
+ (button) =>
1032
+ button.href && (
1033
+ <a href={button.href} {...buttonProps(button)}>
1034
+ {button.text ?? button.href}
1035
+ </a>
1036
+ ),
1037
+ )}
1038
+ </p>
1039
+ )}
1040
+ {src && <img src={src} alt={block.image?.alt ?? ""} />}
1041
+ </section>
1042
+ );
1043
+ }
1044
+ if (block._type === "richTextBlock") {
1045
+ return (
1046
+ <section style={section}>
1047
+ {block.eyebrow && <p style="opacity: 0.6; margin: 0;">{block.eyebrow}</p>}
1048
+ {block.title && <h2>{block.title}</h2>}
1049
+ {block.richText && <PortableText value={block.richText} />}
1050
+ </section>
1051
+ );
1052
+ }
1053
+ if (block._type === "featureCardsIcon") {
1054
+ return (
1055
+ <section style={section}>
1056
+ {block.eyebrow && <p style="opacity: 0.6; margin: 0;">{block.eyebrow}</p>}
1057
+ {block.title && <h2>{block.title}</h2>}
1058
+ {block.richText && <PortableText value={block.richText} />}
1059
+ <div style={grid}>
1060
+ {(block.cards ?? []).map((item) => (
1061
+ <div style={card}>
1062
+ {/* \`icon\` holds a Lucide icon name; render it with your icon set of choice. */}
1063
+ {item.icon && <p style="opacity: 0.6; margin: 0;">{item.icon}</p>}
1064
+ <h3>{item.title}</h3>
1065
+ {item.richText && <PortableText value={item.richText} />}
1066
+ </div>
1067
+ ))}
1068
+ </div>
1069
+ </section>
1070
+ );
1071
+ }
1072
+ if (block._type === "imageLinkCards") {
1073
+ return (
1074
+ <section style={section}>
1075
+ {block.eyebrow && <p style="opacity: 0.6; margin: 0;">{block.eyebrow}</p>}
1076
+ {block.title && <h2>{block.title}</h2>}
1077
+ {block.richText && <PortableText value={block.richText} />}
1078
+ <div style={grid}>
1079
+ {(block.cards ?? []).map((item) => {
1080
+ const src = urlFor(item.image, 800);
1081
+ return (
1082
+ <div style={card}>
1083
+ {src && <img src={src} alt={item.image?.alt ?? ""} />}
1084
+ <h3>
1085
+ {item.href ? (
1086
+ <a
1087
+ href={item.href}
1088
+ {...(item.openInNewTab ? { target: "_blank", rel: "noreferrer" } : {})}
1089
+ >
1090
+ {item.title}
1091
+ </a>
1092
+ ) : (
1093
+ item.title
1094
+ )}
1095
+ </h3>
1096
+ {item.description && <p>{item.description}</p>}
1097
+ </div>
1098
+ );
1099
+ })}
1100
+ </div>
1101
+ </section>
1102
+ );
1103
+ }
1104
+ if (block._type === "faqAccordion") {
1105
+ return (
1106
+ <section style={section}>
1107
+ {block.eyebrow && <p style="opacity: 0.6; margin: 0;">{block.eyebrow}</p>}
1108
+ {block.title && <h2>{block.title}</h2>}
1109
+ {block.subtitle && <p>{block.subtitle}</p>}
1110
+ {(block.faqs ?? []).map((faq) => (
1111
+ <details style="border-top: 1px solid #e5e5e5; padding: 0.75rem 0;">
1112
+ <summary>{faq.title}</summary>
1113
+ {faq.richText && <PortableText value={faq.richText} />}
1114
+ </details>
1115
+ ))}
1116
+ </section>
1117
+ );
1118
+ }
1119
+ if (block._type === "subscribeNewsletter") {
1120
+ // Presentational only: no action, no handler. Wire it to the \`forms\` module's
1121
+ // endpoint (or your own) when you want it to do something.
1122
+ return (
1123
+ <section style={section}>
1124
+ <h2>{block.title}</h2>
1125
+ {block.subTitle && <PortableText value={block.subTitle} />}
1126
+ <form style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
1127
+ <input type="email" name="email" placeholder="you@example.com" required />
1128
+ <button type="submit">Subscribe</button>
1129
+ </form>
1130
+ {block.helperText && <PortableText value={block.helperText} />}
1131
+ </section>
1132
+ );
1133
+ }
1134
+ if (block._type === "cta") {
1135
+ return (
1136
+ <section style={section}>
1137
+ {block.eyebrow && <p style="opacity: 0.6; margin: 0;">{block.eyebrow}</p>}
1138
+ <h2>{block.title}</h2>
1139
+ {block.richText && <PortableText value={block.richText} />}
1140
+ {!!block.buttons?.length && (
1141
+ <p style="display: flex; gap: 0.75rem; flex-wrap: wrap;">
1142
+ {block.buttons.map(
1143
+ (button) =>
1144
+ button.href && (
1145
+ <a href={button.href} {...buttonProps(button)}>
1146
+ {button.text ?? button.href}
1147
+ </a>
1148
+ ),
1149
+ )}
1150
+ </p>
1151
+ )}
1152
+ </section>
1153
+ );
1154
+ }
1155
+ return null;
1156
+ })
1157
+ }
1158
+ `, ASTRO_SLUG_PAGE = `---
1159
+ import { VisualEditing } from "@sanity/astro/visual-editing";
1160
+ import { visualEditingEnabled } from "@workspace/sanity/env";
1161
+ import { loadQuery } from "@workspace/sanity/fetch";
1162
+ import { PAGE_QUERY, PAGE_SLUGS_QUERY } from "@workspace/sanity/queries";
1163
+ import type { PAGE_QUERY_RESULT } from "@workspace/sanity/types";
1164
+ import PageBuilder from "@workspace/sanity-blocks/PageBuilder.astro";
1165
+
1166
+ export async function getStaticPaths() {
1167
+ // Returns [] without credentials, so a fresh scaffold builds with no CMS pages.
1168
+ const slugs = await loadQuery<(string | null)[]>(PAGE_SLUGS_QUERY);
1169
+ return (slugs ?? []).filter(Boolean).map((slug) => ({ params: { slug } }));
1170
+ }
1171
+
1172
+ const page = await loadQuery<PAGE_QUERY_RESULT>(PAGE_QUERY, { slug: Astro.params.slug });
1173
+ ---
1174
+
1175
+ <html lang="en">
1176
+ <head>
1177
+ <meta charset="utf-8" />
1178
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
1179
+ <title>{page?.title ?? "web"}</title>
1180
+ </head>
1181
+ <body style="font-family: system-ui, sans-serif; padding: 3rem;">
1182
+ <h1>{page?.title}</h1>
1183
+ <PageBuilder blocks={page?.pageBuilder ?? []} />
1184
+ <VisualEditing enabled={visualEditingEnabled} />
1185
+ </body>
1186
+ </html>
1187
+ `, ASTRO_BLOG_INDEX_PAGE = `---
1188
+ import { VisualEditing } from "@sanity/astro/visual-editing";
1189
+ import { visualEditingEnabled } from "@workspace/sanity/env";
1190
+ import { loadQuery } from "@workspace/sanity/fetch";
1191
+ import { urlFor } from "@workspace/sanity/image";
1192
+ import { BLOG_INDEX_QUERY } from "@workspace/sanity/queries";
1193
+ import type { BLOG_INDEX_QUERY_RESULT } from "@workspace/sanity/types";
1194
+
1195
+ const posts = (await loadQuery<BLOG_INDEX_QUERY_RESULT>(BLOG_INDEX_QUERY)) ?? [];
1196
+ ---
1197
+
1198
+ <html lang="en">
1199
+ <head>
1200
+ <meta charset="utf-8" />
1201
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
1202
+ <title>Blog</title>
1203
+ </head>
1204
+ <body style="font-family: system-ui, sans-serif; padding: 3rem;">
1205
+ <h1>Blog</h1>
1206
+ {
1207
+ posts.length === 0 && (
1208
+ <p>
1209
+ No posts yet — seed the dataset with <code>pnpm sanity:seed</code> from apps/studio,
1210
+ or write one in the Studio (<code>pnpm dev:studio</code>).
1211
+ </p>
1212
+ )
1213
+ }
1214
+ {
1215
+ posts.map((post) => {
1216
+ const src = urlFor(post.image, 800);
1217
+ const author = post.authors?.[0];
1218
+ return (
1219
+ <article style="border-top: 1px solid #e5e5e5; padding: 1.5rem 0;">
1220
+ {src && <img src={src} alt={post.image?.alt ?? ""} />}
1221
+ <h2>
1222
+ <a href={\`/blog/\${post.slug}\`}>{post.title}</a>
1223
+ </h2>
1224
+ <p style="opacity: 0.6;">
1225
+ {author?.name}
1226
+ {author?.name && post.publishedAt && " · "}
1227
+ {post.publishedAt && <time datetime={post.publishedAt}>{post.publishedAt}</time>}
1228
+ </p>
1229
+ {post.description && <p>{post.description}</p>}
1230
+ </article>
1231
+ );
1232
+ })
1233
+ }
1234
+ <VisualEditing enabled={visualEditingEnabled} />
1235
+ </body>
1236
+ </html>
1237
+ `, ASTRO_BLOG_POST_PAGE = `---
1238
+ import { VisualEditing } from "@sanity/astro/visual-editing";
1239
+ import { visualEditingEnabled } from "@workspace/sanity/env";
1240
+ import { loadQuery } from "@workspace/sanity/fetch";
1241
+ import { urlFor } from "@workspace/sanity/image";
1242
+ import { BLOG_QUERY, BLOG_SLUGS_QUERY } from "@workspace/sanity/queries";
1243
+ import type { BLOG_QUERY_RESULT } from "@workspace/sanity/types";
1244
+ import { PortableText } from "astro-portabletext";
1245
+
1246
+ export async function getStaticPaths() {
1247
+ // Returns [] without credentials, so a fresh scaffold builds with no posts.
1248
+ const slugs = await loadQuery<(string | null)[]>(BLOG_SLUGS_QUERY);
1249
+ return (slugs ?? []).filter(Boolean).map((slug) => ({ params: { slug } }));
1250
+ }
1251
+
1252
+ const post = await loadQuery<BLOG_QUERY_RESULT>(BLOG_QUERY, { slug: Astro.params.slug });
1253
+ const src = urlFor(post?.image, 1600);
1254
+ const author = post?.authors?.[0];
1255
+ ---
1256
+
1257
+ <html lang="en">
1258
+ <head>
1259
+ <meta charset="utf-8" />
1260
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
1261
+ <title>{post?.title ?? "Blog"}</title>
1262
+ {post?.description && <meta name="description" content={post.description} />}
1263
+ </head>
1264
+ <body style="font-family: system-ui, sans-serif; padding: 3rem;">
1265
+ <p><a href="/blog">← Blog</a></p>
1266
+ <h1>{post?.title}</h1>
1267
+ <p style="opacity: 0.6;">
1268
+ {author?.name && (author.position ? \`\${author.name}, \${author.position}\` : author.name)}
1269
+ {author?.name && post?.publishedAt && " · "}
1270
+ {post?.publishedAt && <time datetime={post.publishedAt}>{post.publishedAt}</time>}
1271
+ </p>
1272
+ {src && <img src={src} alt={post?.image?.alt ?? ""} />}
1273
+ {post?.richText && <PortableText value={post.richText} />}
1274
+ <VisualEditing enabled={visualEditingEnabled} />
1275
+ </body>
1276
+ </html>
1277
+ `, 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.
1278
+ try {
1279
+ process.loadEnvFile(".env");
1280
+ } catch {
1281
+ // No .env — the placeholder ids below keep the build working.
1282
+ }
1283
+ `;
1284
+
1285
+ // ../../packages/registry/src/content/sanity/next.ts
1286
+ var NEXT_ENV = `export const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID || "placeholder";
1287
+
1288
+ export const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET || "production";
1289
+
1290
+ // A recent pinned date, per Sanity's API-versioning guidance (the docs pin the date the
1291
+ // project was set up; bump deliberately, never to "today").
1292
+ export const apiVersion = process.env.NEXT_PUBLIC_SANITY_API_VERSION || "2026-02-01";
1293
+
1294
+ /** Server-only read token; empty when unset (draft mode and live previews need it). */
1295
+ export const readToken = process.env.SANITY_API_READ_TOKEN ?? "";
1296
+
1297
+ /**
1298
+ * Where the Studio lives — stega encodes it into every string it returns, so a
1299
+ * Visual Editing click-through lands on the right field. Absolute since the Studio
1300
+ * became its own app (\`sanity dev\` defaults to :3333).
1301
+ */
1302
+ export const studioUrl = process.env.NEXT_PUBLIC_SANITY_STUDIO_URL || "http://localhost:3333";
1303
+
1304
+ /**
1305
+ * False on a freshly scaffolded project. Guards every fetch so the build never dials
1095
1306
  * placeholder.api.sanity.io and dies; pages render an empty state instead.
1096
1307
  */
1097
1308
  export const hasSanityCredentials = projectId !== "placeholder";
@@ -1109,17 +1320,33 @@ export const client = createClient({
1109
1320
  stega: { studioUrl },
1110
1321
  });
1111
1322
  `, NEXT_LIVE = `import { defineLive } from "next-sanity/live";
1112
- import { readToken } from "./env";
1323
+ import { hasSanityCredentials, readToken } from "./env";
1113
1324
  import { client } from "./client";
1114
1325
 
1115
1326
  // defineLive gives us live content plus draft-mode reads. The token is passed as
1116
1327
  // undefined rather than "" when unset — an empty string reads as "provided but invalid".
1117
- export const { sanityFetch, SanityLive } = defineLive({
1328
+ export const { sanityFetch, SanityLive: LiveComponent } = defineLive({
1118
1329
  client,
1119
1330
  serverToken: readToken || undefined,
1120
1331
  browserToken: readToken || undefined,
1121
1332
  });
1122
- `, NEXT_FETCH = `import type { QueryParams } from "next-sanity";
1333
+
1334
+ /**
1335
+ * On a fresh scaffold \`projectId\` is "placeholder", so the live socket dials
1336
+ * placeholder.api.sanity.io and next-sanity retries forever — the dev log fills with
1337
+ * "<SanityLive> is attempting to reconnect" and nothing else is readable. Render
1338
+ * nothing until there are credentials, the same guard \`loadQuery\` applies to reads.
1339
+ */
1340
+ export const SanityLive = hasSanityCredentials ? LiveComponent : () => null;
1341
+ `, NEXT_FETCH = `/// <reference path="./sanity.types.ts" />
1342
+ // ^ \`sanity typegen generate\` writes sanity.types.ts here (see apps/studio/sanity.cli.ts) and
1343
+ // its \`declare module "@sanity/client"\` block is what types every query result. Now that
1344
+ // the file lives outside apps/web it is no longer swept up by the app's tsconfig include,
1345
+ // so this reference is what pulls it into the program — without it every loadQuery result
1346
+ // silently degrades to \`unknown\` and the pages stop type-checking. A path reference cannot
1347
+ // be elided the way an unused import can. The file ships pre-generated; regenerate it with
1348
+ // \`extract\` + \`type\` from apps/studio.
1349
+ import type { QueryParams } from "next-sanity";
1123
1350
  import { client } from "./client";
1124
1351
  import { hasSanityCredentials } from "./env";
1125
1352
  import { sanityFetch } from "./live";
@@ -1173,52 +1400,234 @@ export function urlFor(source: unknown, width = 1200): string | null {
1173
1400
  return builder.image(ref).width(width).auto("format").url();
1174
1401
  }
1175
1402
  `, NEXT_PAGE_BUILDER = `import { PortableText, type PortableTextProps } from "@portabletext/react";
1403
+ import { urlFor } from "@workspace/sanity/image";
1176
1404
  import Image from "next/image";
1177
- import { urlFor } from "./image";
1178
1405
 
1179
1406
  type SanityImage = { asset?: { _ref?: string } | null; alt?: string | null } | null;
1407
+ type Rich = PortableTextProps["value"] | null;
1408
+
1409
+ /** queries.ts flattens every \`customUrl\` to an href, so nothing here branches on link type. */
1410
+ type ButtonLink = {
1411
+ _key: string;
1412
+ text?: string | null;
1413
+ variant?: string | null;
1414
+ href?: string | null;
1415
+ openInNewTab?: boolean | null;
1416
+ };
1180
1417
 
1181
1418
  export type PageBlock =
1182
- | { _key: string; _type: "hero"; heading?: string; subheading?: string; image?: SanityImage }
1183
- | { _key: string; _type: "richText"; heading?: string; content?: PortableTextProps["value"] }
1419
+ | {
1420
+ _key: string;
1421
+ _type: "hero";
1422
+ badge?: string | null;
1423
+ title?: string | null;
1424
+ richText?: Rich;
1425
+ image?: SanityImage;
1426
+ buttons?: ButtonLink[] | null;
1427
+ }
1428
+ | {
1429
+ _key: string;
1430
+ _type: "richTextBlock";
1431
+ eyebrow?: string | null;
1432
+ title?: string | null;
1433
+ richText?: Rich;
1434
+ }
1435
+ | {
1436
+ _key: string;
1437
+ _type: "featureCardsIcon";
1438
+ eyebrow?: string | null;
1439
+ title?: string | null;
1440
+ richText?: Rich;
1441
+ cards?:
1442
+ | { _key: string; icon?: string | null; title?: string | null; richText?: Rich }[]
1443
+ | null;
1444
+ }
1445
+ | {
1446
+ _key: string;
1447
+ _type: "imageLinkCards";
1448
+ eyebrow?: string | null;
1449
+ title?: string | null;
1450
+ richText?: Rich;
1451
+ buttons?: ButtonLink[] | null;
1452
+ cards?:
1453
+ | {
1454
+ _key: string;
1455
+ title?: string | null;
1456
+ description?: string | null;
1457
+ image?: SanityImage;
1458
+ href?: string | null;
1459
+ openInNewTab?: boolean | null;
1460
+ }[]
1461
+ | null;
1462
+ }
1463
+ | {
1464
+ _key: string;
1465
+ _type: "faqAccordion";
1466
+ eyebrow?: string | null;
1467
+ title?: string | null;
1468
+ subtitle?: string | null;
1469
+ faqs?: { _id: string; title?: string | null; richText?: Rich }[] | null;
1470
+ }
1471
+ | {
1472
+ _key: string;
1473
+ _type: "subscribeNewsletter";
1474
+ title?: string | null;
1475
+ subTitle?: Rich;
1476
+ helperText?: Rich;
1477
+ }
1184
1478
  | {
1185
1479
  _key: string;
1186
1480
  _type: "cta";
1187
- heading?: string;
1188
- text?: string;
1189
- buttonLabel?: string;
1190
- buttonHref?: string;
1481
+ eyebrow?: string | null;
1482
+ title?: string | null;
1483
+ richText?: Rich;
1484
+ buttons?: ButtonLink[] | null;
1191
1485
  };
1192
1486
 
1487
+ const section = { margin: "3rem 0" } as const;
1488
+ const grid = {
1489
+ display: "grid",
1490
+ gap: "1.5rem",
1491
+ gridTemplateColumns: "repeat(auto-fit, minmax(16rem, 1fr))",
1492
+ } as const;
1493
+ const card = { border: "1px solid #e5e5e5", borderRadius: "0.75rem", padding: "1.25rem" } as const;
1494
+
1495
+ function Eyebrow({ children }: { children?: string | null }) {
1496
+ return children ? <p style={{ opacity: 0.6, margin: 0 }}>{children}</p> : null;
1497
+ }
1498
+
1499
+ function Buttons({ items }: { items?: ButtonLink[] | null }) {
1500
+ if (!items?.length) return null;
1501
+ return (
1502
+ <p style={{ display: "flex", gap: "0.75rem", flexWrap: "wrap" }}>
1503
+ {items.map((button) =>
1504
+ button.href ? (
1505
+ <a
1506
+ key={button._key}
1507
+ href={button.href}
1508
+ {...(button.openInNewTab ? { target: "_blank", rel: "noreferrer" } : {})}
1509
+ >
1510
+ {button.text ?? button.href}
1511
+ </a>
1512
+ ) : null,
1513
+ )}
1514
+ </p>
1515
+ );
1516
+ }
1517
+
1193
1518
  function Block({ block }: { block: PageBlock }) {
1194
1519
  switch (block._type) {
1195
1520
  case "hero": {
1196
1521
  const src = urlFor(block.image, 1600);
1197
1522
  return (
1198
- <section>
1199
- <h2>{block.heading}</h2>
1200
- {block.subheading ? <p>{block.subheading}</p> : null}
1523
+ <section style={section}>
1524
+ <Eyebrow>{block.badge}</Eyebrow>
1525
+ <h2>{block.title}</h2>
1526
+ {block.richText ? <PortableText value={block.richText} /> : null}
1527
+ <Buttons items={block.buttons} />
1201
1528
  {src ? (
1202
1529
  <Image src={src} alt={block.image?.alt ?? ""} width={1600} height={900} />
1203
1530
  ) : null}
1204
1531
  </section>
1205
1532
  );
1206
1533
  }
1207
- case "richText":
1534
+ case "richTextBlock":
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
+ </section>
1541
+ );
1542
+ case "featureCardsIcon":
1543
+ return (
1544
+ <section style={section}>
1545
+ <Eyebrow>{block.eyebrow}</Eyebrow>
1546
+ {block.title ? <h2>{block.title}</h2> : null}
1547
+ {block.richText ? <PortableText value={block.richText} /> : null}
1548
+ <div style={grid}>
1549
+ {(block.cards ?? []).map((item) => (
1550
+ <div key={item._key} style={card}>
1551
+ {/* \`icon\` holds a Lucide icon name; render it with your icon set of choice. */}
1552
+ {item.icon ? <p style={{ opacity: 0.6, margin: 0 }}>{item.icon}</p> : null}
1553
+ <h3>{item.title}</h3>
1554
+ {item.richText ? <PortableText value={item.richText} /> : null}
1555
+ </div>
1556
+ ))}
1557
+ </div>
1558
+ </section>
1559
+ );
1560
+ case "imageLinkCards":
1561
+ return (
1562
+ <section style={section}>
1563
+ <Eyebrow>{block.eyebrow}</Eyebrow>
1564
+ {block.title ? <h2>{block.title}</h2> : null}
1565
+ {block.richText ? <PortableText value={block.richText} /> : null}
1566
+ <div style={grid}>
1567
+ {(block.cards ?? []).map((item) => {
1568
+ const src = urlFor(item.image, 800);
1569
+ return (
1570
+ <div key={item._key} style={card}>
1571
+ {src ? (
1572
+ <Image src={src} alt={item.image?.alt ?? ""} width={800} height={450} />
1573
+ ) : null}
1574
+ <h3>
1575
+ {item.href ? (
1576
+ <a
1577
+ href={item.href}
1578
+ {...(item.openInNewTab ? { target: "_blank", rel: "noreferrer" } : {})}
1579
+ >
1580
+ {item.title}
1581
+ </a>
1582
+ ) : (
1583
+ item.title
1584
+ )}
1585
+ </h3>
1586
+ {item.description ? <p>{item.description}</p> : null}
1587
+ </div>
1588
+ );
1589
+ })}
1590
+ </div>
1591
+ <Buttons items={block.buttons} />
1592
+ </section>
1593
+ );
1594
+ case "faqAccordion":
1595
+ // <details> is the accordion — it needs no client component and behaves the same
1596
+ // in the Astro build, where this same markup is duplicated.
1597
+ return (
1598
+ <section style={section}>
1599
+ <Eyebrow>{block.eyebrow}</Eyebrow>
1600
+ {block.title ? <h2>{block.title}</h2> : null}
1601
+ {block.subtitle ? <p>{block.subtitle}</p> : null}
1602
+ {(block.faqs ?? []).map((faq) => (
1603
+ <details key={faq._id} style={{ borderTop: "1px solid #e5e5e5", padding: "0.75rem 0" }}>
1604
+ <summary>{faq.title}</summary>
1605
+ {faq.richText ? <PortableText value={faq.richText} /> : null}
1606
+ </details>
1607
+ ))}
1608
+ </section>
1609
+ );
1610
+ case "subscribeNewsletter":
1611
+ // Presentational only: no action, no handler. Wire it to the \`forms\` module's
1612
+ // endpoint (or your own) when you want it to do something.
1208
1613
  return (
1209
- <section>
1210
- {block.heading ? <h2>{block.heading}</h2> : null}
1211
- {block.content ? <PortableText value={block.content} /> : null}
1614
+ <section style={section}>
1615
+ <h2>{block.title}</h2>
1616
+ {block.subTitle ? <PortableText value={block.subTitle} /> : null}
1617
+ <form style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap" }}>
1618
+ <input type="email" name="email" placeholder="you@example.com" required />
1619
+ <button type="submit">Subscribe</button>
1620
+ </form>
1621
+ {block.helperText ? <PortableText value={block.helperText} /> : null}
1212
1622
  </section>
1213
1623
  );
1214
1624
  case "cta":
1215
1625
  return (
1216
- <section>
1217
- <h2>{block.heading}</h2>
1218
- {block.text ? <p>{block.text}</p> : null}
1219
- {block.buttonHref && block.buttonLabel ? (
1220
- <a href={block.buttonHref}>{block.buttonLabel}</a>
1221
- ) : null}
1626
+ <section style={section}>
1627
+ <Eyebrow>{block.eyebrow}</Eyebrow>
1628
+ <h2>{block.title}</h2>
1629
+ {block.richText ? <PortableText value={block.richText} /> : null}
1630
+ <Buttons items={block.buttons} />
1222
1631
  </section>
1223
1632
  );
1224
1633
  default:
@@ -1236,34 +1645,9 @@ export function PageBuilder({ blocks }: { blocks: PageBlock[] }) {
1236
1645
  </>
1237
1646
  );
1238
1647
  }
1239
- `, NEXT_SANITY_CONFIG, NEXT_SANITY_CLI = `import { defineCliConfig } from "sanity/cli";
1240
-
1241
- // Run from apps/web. \`schema extract\` + \`typegen generate\` need no credentials, so the
1242
- // build-time \`prebuild\` hook works on a fresh clone.
1243
- export default defineCliConfig({
1244
- api: {
1245
- projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID || "placeholder",
1246
- dataset: process.env.NEXT_PUBLIC_SANITY_DATASET || "production",
1247
- },
1248
- typegen: {
1249
- path: "./sanity/**/*.{ts,tsx}",
1250
- schema: "sanity/schema.json",
1251
- generates: "sanity/sanity.types.ts",
1252
- },
1253
- });
1254
- `, NEXT_STUDIO_PAGE = `import { NextStudio } from "next-sanity/studio";
1255
- import config from "../../../sanity.config";
1256
-
1257
- export const dynamic = "force-static";
1258
-
1259
- export { metadata, viewport } from "next-sanity/studio";
1260
-
1261
- export default function StudioPage() {
1262
- return <NextStudio config={config} />;
1263
- }
1264
- `, NEXT_DRAFT_MODE_ROUTE = `import { defineEnableDraftMode } from "next-sanity/draft-mode";
1265
- import { readToken } from "../../../../sanity/env";
1266
- import { client } from "../../../../sanity/client";
1648
+ `, NEXT_DRAFT_MODE_ROUTE = `import { client } from "@workspace/sanity/client";
1649
+ import { readToken } from "@workspace/sanity/env";
1650
+ import { defineEnableDraftMode } from "next-sanity/draft-mode";
1267
1651
 
1268
1652
  export const { GET } = defineEnableDraftMode({
1269
1653
  client: client.withConfig({ token: readToken }),
@@ -1301,10 +1685,10 @@ export function DisableDraftMode() {
1301
1685
  </a>
1302
1686
  );
1303
1687
  }
1304
- `, NEXT_HOME_PAGE = `import type { Metadata } from "next";
1305
- import { loadQuery } from "../sanity/fetch";
1306
- import { PageBuilder, type PageBlock } from "../sanity/page-builder";
1307
- import { HOME_PAGE_QUERY, SETTINGS_QUERY } from "../sanity/queries";
1688
+ `, NEXT_HOME_PAGE = `import { loadQuery } from "@workspace/sanity/fetch";
1689
+ import { HOME_PAGE_QUERY, SETTINGS_QUERY } from "@workspace/sanity/queries";
1690
+ import { PageBuilder, type PageBlock } from "@workspace/sanity-blocks/blocks";
1691
+ import type { Metadata } from "next";
1308
1692
 
1309
1693
  // stega: false — encoded metadata would leak invisible characters into <title>.
1310
1694
  export async function generateMetadata(): Promise<Metadata> {
@@ -1324,7 +1708,8 @@ export default async function HomePage() {
1324
1708
  <h1>No content yet</h1>
1325
1709
  <p>
1326
1710
  Set NEXT_PUBLIC_SANITY_PROJECT_ID in apps/web/.env.local, then seed the dataset:
1327
- <code> pnpm sanity:seed</code> from apps/web. Edit it at <a href="/studio">/studio</a>.
1711
+ <code> pnpm sanity:seed</code> from apps/studio. Edit it in the Studio:
1712
+ <code> pnpm dev:studio</code>.
1328
1713
  </p>
1329
1714
  </main>
1330
1715
  );
@@ -1337,10 +1722,10 @@ export default async function HomePage() {
1337
1722
  </main>
1338
1723
  );
1339
1724
  }
1340
- `, NEXT_SLUG_PAGE = `import { notFound } from "next/navigation";
1341
- import { loadQuery, loadStaticQuery } from "../../sanity/fetch";
1342
- import { PageBuilder, type PageBlock } from "../../sanity/page-builder";
1343
- import { PAGE_QUERY, PAGE_SLUGS_QUERY } from "../../sanity/queries";
1725
+ `, NEXT_SLUG_PAGE = `import { loadQuery, loadStaticQuery } from "@workspace/sanity/fetch";
1726
+ import { PAGE_QUERY, PAGE_SLUGS_QUERY } from "@workspace/sanity/queries";
1727
+ import { PageBuilder, type PageBlock } from "@workspace/sanity-blocks/blocks";
1728
+ import { notFound } from "next/navigation";
1344
1729
 
1345
1730
  export async function generateStaticParams() {
1346
1731
  const slugs = await loadStaticQuery(PAGE_SLUGS_QUERY);
@@ -1359,193 +1744,1877 @@ export default async function Page({ params }: { params: Promise<{ slug: string
1359
1744
  </main>
1360
1745
  );
1361
1746
  }
1362
- `, ASTRO_ENV = `export const projectId = import.meta.env.PUBLIC_SANITY_PROJECT_ID || "placeholder";
1747
+ `, NEXT_BLOG_INDEX_PAGE = `import { loadQuery } from "@workspace/sanity/fetch";
1748
+ import { urlFor } from "@workspace/sanity/image";
1749
+ import { BLOG_INDEX_QUERY } from "@workspace/sanity/queries";
1750
+ import type { Metadata } from "next";
1751
+ import Image from "next/image";
1363
1752
 
1364
- export const dataset = import.meta.env.PUBLIC_SANITY_DATASET || "production";
1753
+ export const metadata: Metadata = { title: "Blog" };
1365
1754
 
1366
- /** Server-only read token; required (and only read) when Visual Editing is enabled. */
1367
- export const readToken = import.meta.env.SANITY_API_READ_TOKEN ?? "";
1755
+ export default async function BlogIndexPage() {
1756
+ const posts = (await loadQuery(BLOG_INDEX_QUERY)) ?? [];
1368
1757
 
1369
- /**
1370
- * The @sanity/astro Visual Editing switch. Off by default: overlays need a rendering
1371
- * server (\`astro dev\`, or an SSR adapter in production) plus SANITY_API_READ_TOKEN.
1372
- */
1373
- export const visualEditingEnabled =
1374
- import.meta.env.PUBLIC_SANITY_VISUAL_EDITING_ENABLED === "true";
1758
+ return (
1759
+ <main style={{ fontFamily: "system-ui, sans-serif", padding: "3rem" }}>
1760
+ <h1>Blog</h1>
1761
+ {posts.length === 0 ? (
1762
+ <p>
1763
+ No posts yet — seed the dataset with <code>pnpm sanity:seed</code> from apps/studio,
1764
+ or write one in the Studio (<code>pnpm dev:studio</code>).
1765
+ </p>
1766
+ ) : null}
1767
+ {posts.map((post) => {
1768
+ const src = urlFor(post.image, 800);
1769
+ const author = post.authors?.[0];
1770
+ return (
1771
+ <article key={post._id} style={{ borderTop: "1px solid #e5e5e5", padding: "1.5rem 0" }}>
1772
+ {src ? <Image src={src} alt={post.image?.alt ?? ""} width={800} height={450} /> : null}
1773
+ <h2>
1774
+ <a href={\`/blog/\${post.slug}\`}>{post.title}</a>
1775
+ </h2>
1776
+ <p style={{ opacity: 0.6 }}>
1777
+ {author?.name ? <span>{author.name}</span> : null}
1778
+ {author?.name && post.publishedAt ? " · " : null}
1779
+ {post.publishedAt ? <time dateTime={post.publishedAt}>{post.publishedAt}</time> : null}
1780
+ </p>
1781
+ {post.description ? <p>{post.description}</p> : null}
1782
+ </article>
1783
+ );
1784
+ })}
1785
+ </main>
1786
+ );
1787
+ }
1788
+ `, NEXT_BLOG_POST_PAGE = `import { PortableText } from "@portabletext/react";
1789
+ import { loadQuery, loadStaticQuery } from "@workspace/sanity/fetch";
1790
+ import { urlFor } from "@workspace/sanity/image";
1791
+ import { BLOG_QUERY, BLOG_SLUGS_QUERY } from "@workspace/sanity/queries";
1792
+ import type { Metadata } from "next";
1793
+ import Image from "next/image";
1794
+ import { notFound } from "next/navigation";
1375
1795
 
1376
- /**
1377
- * False on a freshly scaffolded project. Guards every fetch so \`astro build\` never dials
1378
- * placeholder.api.sanity.io and dies; pages render an empty state instead.
1379
- */
1380
- export const hasSanityCredentials = projectId !== "placeholder";
1381
- `, ASTRO_FETCH = `import { sanityClient } from "sanity:client";
1382
- import { hasSanityCredentials, readToken, visualEditingEnabled } from "./env";
1796
+ export async function generateStaticParams() {
1797
+ const slugs = await loadStaticQuery(BLOG_SLUGS_QUERY);
1798
+ return (slugs ?? []).filter((slug): slug is string => Boolean(slug)).map((slug) => ({ slug }));
1799
+ }
1800
+
1801
+ // stega: false encoded metadata would leak invisible characters into <title>.
1802
+ export async function generateMetadata({
1803
+ params,
1804
+ }: {
1805
+ params: Promise<{ slug: string }>;
1806
+ }): Promise<Metadata> {
1807
+ const { slug } = await params;
1808
+ const post = await loadQuery(BLOG_QUERY, { slug }, { stega: false });
1809
+ return { title: post?.title ?? "Blog", description: post?.description ?? undefined };
1810
+ }
1811
+
1812
+ export default async function BlogPostPage({ params }: { params: Promise<{ slug: string }> }) {
1813
+ const { slug } = await params;
1814
+ const post = await loadQuery(BLOG_QUERY, { slug });
1815
+ if (!post) notFound();
1816
+
1817
+ const src = urlFor(post.image, 1600);
1818
+ const author = post.authors?.[0];
1819
+
1820
+ return (
1821
+ <main style={{ fontFamily: "system-ui, sans-serif", padding: "3rem" }}>
1822
+ <p>
1823
+ <a href="/blog">← Blog</a>
1824
+ </p>
1825
+ <h1>{post.title}</h1>
1826
+ <p style={{ opacity: 0.6 }}>
1827
+ {author?.name ? <span>{author.position ? \`\${author.name}, \${author.position}\` : author.name}</span> : null}
1828
+ {author?.name && post.publishedAt ? " · " : null}
1829
+ {post.publishedAt ? <time dateTime={post.publishedAt}>{post.publishedAt}</time> : null}
1830
+ </p>
1831
+ {src ? <Image src={src} alt={post.image?.alt ?? ""} width={1600} height={900} /> : null}
1832
+ {post.richText ? <PortableText value={post.richText} /> : null}
1833
+ </main>
1834
+ );
1835
+ }
1836
+ `;
1837
+
1838
+ // ../../packages/registry/src/content/sanity/schema.ts
1839
+ var SCHEMA_DEFINITIONS = `import { defineArrayMember, defineField, defineType } from "sanity";
1383
1840
 
1384
1841
  /**
1385
- * The single read path, following @sanity/astro's Visual Editing recipe: with
1386
- * PUBLIC_SANITY_VISUAL_EDITING_ENABLED=true it fetches the drafts perspective with
1387
- * stega source maps, which is what gives Presentation its click-to-edit overlays.
1388
- *
1389
- * Without credentials it returns null before touching the network, so \`astro build\`
1390
- * succeeds on a fresh scaffold. The catch covers a configured-but-unreachable dataset
1391
- * for the same reason: a starter should render empty, not crash.
1842
+ * A link that resolves to either an internal document or an external URL. Blocks store the
1843
+ * choice; queries.ts turns it into a plain \`href\` string so components never branch on it.
1844
+ */
1845
+ export const customUrl = defineType({
1846
+ name: "customUrl",
1847
+ title: "URL",
1848
+ type: "object",
1849
+ fields: [
1850
+ defineField({
1851
+ name: "type",
1852
+ type: "string",
1853
+ initialValue: () => "external",
1854
+ options: { list: ["internal", "external"], layout: "radio", direction: "horizontal" },
1855
+ validation: (rule) => rule.required(),
1856
+ }),
1857
+ defineField({
1858
+ name: "openInNewTab",
1859
+ title: "Open in new tab",
1860
+ type: "boolean",
1861
+ initialValue: () => false,
1862
+ }),
1863
+ defineField({
1864
+ name: "external",
1865
+ title: "URL",
1866
+ type: "string",
1867
+ description: "A full address (https://…) or a path (/about).",
1868
+ hidden: ({ parent }) => parent?.type !== "external",
1869
+ validation: (rule) =>
1870
+ rule.custom((value, { parent }) =>
1871
+ (parent as { type?: string })?.type === "external" && !value
1872
+ ? "URL can't be empty"
1873
+ : true,
1874
+ ),
1875
+ }),
1876
+ defineField({
1877
+ name: "internal",
1878
+ type: "reference",
1879
+ to: [{ type: "page" }, { type: "blog" }],
1880
+ options: { disableNew: true },
1881
+ hidden: ({ parent }) => parent?.type !== "internal",
1882
+ validation: (rule) =>
1883
+ rule.custom((value, { parent }) =>
1884
+ (parent as { type?: string })?.type === "internal" && !value?._ref
1885
+ ? "Pick a document to link to"
1886
+ : true,
1887
+ ),
1888
+ }),
1889
+ ],
1890
+ preview: {
1891
+ select: { type: "type", external: "external", internal: "internal.slug.current" },
1892
+ prepare: ({ type, external, internal }) => ({
1893
+ title: type === "external" ? "External link" : "Internal link",
1894
+ subtitle: type === "external" ? external : internal,
1895
+ }),
1896
+ },
1897
+ });
1898
+
1899
+ export const button = defineType({
1900
+ name: "button",
1901
+ title: "Button",
1902
+ type: "object",
1903
+ fields: [
1904
+ defineField({
1905
+ name: "variant",
1906
+ type: "string",
1907
+ initialValue: () => "default",
1908
+ options: {
1909
+ list: ["default", "secondary", "outline", "link"],
1910
+ layout: "radio",
1911
+ direction: "horizontal",
1912
+ },
1913
+ }),
1914
+ defineField({ name: "text", title: "Button text", type: "string" }),
1915
+ defineField({ name: "url", title: "URL", type: "customUrl" }),
1916
+ ],
1917
+ preview: {
1918
+ select: { title: "text", variant: "variant" },
1919
+ prepare: ({ title, variant }) => ({
1920
+ title: title ?? "Untitled button",
1921
+ subtitle: variant ?? "default",
1922
+ }),
1923
+ },
1924
+ });
1925
+
1926
+ /**
1927
+ * Portable Text. Registered as a named type so documents and blocks can say
1928
+ * \`type: "richText"\` — the same indirection the reference uses.
1929
+ */
1930
+ export const richText = defineType({
1931
+ name: "richText",
1932
+ type: "array",
1933
+ of: [
1934
+ defineArrayMember({
1935
+ name: "block",
1936
+ type: "block",
1937
+ styles: [
1938
+ { title: "Normal", value: "normal" },
1939
+ { title: "H2", value: "h2" },
1940
+ { title: "H3", value: "h3" },
1941
+ { title: "H4", value: "h4" },
1942
+ ],
1943
+ lists: [
1944
+ { title: "Bullet", value: "bullet" },
1945
+ { title: "Numbered", value: "number" },
1946
+ ],
1947
+ marks: {
1948
+ decorators: [
1949
+ { title: "Strong", value: "strong" },
1950
+ { title: "Emphasis", value: "em" },
1951
+ { title: "Code", value: "code" },
1952
+ ],
1953
+ annotations: [
1954
+ {
1955
+ name: "customLink",
1956
+ title: "Link",
1957
+ type: "object",
1958
+ fields: [defineField({ name: "customLink", type: "customUrl" })],
1959
+ },
1960
+ ],
1961
+ },
1962
+ }),
1963
+ defineArrayMember({
1964
+ name: "image",
1965
+ title: "Image",
1966
+ type: "image",
1967
+ options: { hotspot: true },
1968
+ fields: [defineField({ name: "caption", title: "Caption", type: "string" })],
1969
+ }),
1970
+ ],
1971
+ });
1972
+ `, SCHEMA_BLOCKS = `import { defineArrayMember, defineField, defineType } from "sanity";
1973
+
1974
+ // Page-builder blocks are objects, not documents: they only exist inside a page.
1975
+ // Field names match turbo-start-sanity's, so its components drop in unchanged.
1976
+
1977
+ const imageField = defineField({
1978
+ name: "image",
1979
+ type: "image",
1980
+ options: { hotspot: true },
1981
+ fields: [
1982
+ defineField({
1983
+ name: "alt",
1984
+ title: "Alt text",
1985
+ type: "string",
1986
+ validation: (rule) =>
1987
+ rule.custom((value, context) =>
1988
+ (context.parent as { asset?: unknown })?.asset && !value?.trim()
1989
+ ? "Alt text is required when an image is set"
1990
+ : true,
1991
+ ),
1992
+ }),
1993
+ ],
1994
+ });
1995
+
1996
+ const buttonsField = defineField({
1997
+ name: "buttons",
1998
+ type: "array",
1999
+ of: [defineArrayMember({ type: "button" })],
2000
+ });
2001
+
2002
+ export const hero = defineType({
2003
+ name: "hero",
2004
+ title: "Hero",
2005
+ type: "object",
2006
+ fields: [
2007
+ defineField({ name: "badge", type: "string" }),
2008
+ defineField({ name: "title", type: "string" }),
2009
+ defineField({ name: "richText", type: "richText" }),
2010
+ imageField,
2011
+ buttonsField,
2012
+ ],
2013
+ preview: {
2014
+ select: { title: "title", media: "image" },
2015
+ prepare: ({ title, media }) => ({ title: title ?? "Hero", subtitle: "Hero", media }),
2016
+ },
2017
+ });
2018
+
2019
+ export const cta = defineType({
2020
+ name: "cta",
2021
+ title: "Call to action",
2022
+ type: "object",
2023
+ fields: [
2024
+ defineField({ name: "eyebrow", type: "string" }),
2025
+ defineField({ name: "title", type: "string" }),
2026
+ defineField({ name: "richText", type: "richText" }),
2027
+ buttonsField,
2028
+ ],
2029
+ preview: {
2030
+ select: { title: "title" },
2031
+ prepare: ({ title }) => ({ title: title ?? "Call to action", subtitle: "CTA" }),
2032
+ },
2033
+ });
2034
+
2035
+ export const richTextBlock = defineType({
2036
+ name: "richTextBlock",
2037
+ title: "Rich text",
2038
+ type: "object",
2039
+ fields: [
2040
+ defineField({ name: "eyebrow", type: "string" }),
2041
+ defineField({ name: "title", type: "string" }),
2042
+ defineField({ name: "richText", type: "richText" }),
2043
+ ],
2044
+ preview: {
2045
+ select: { title: "title" },
2046
+ prepare: ({ title }) => ({ title: title ?? "Rich text", subtitle: "Rich text" }),
2047
+ },
2048
+ });
2049
+
2050
+ // The reference stores \`icon\` as a \`lucide-icon\`, a custom input from
2051
+ // sanity-plugin-lucide-icon-picker. A plain string keeps the field (and the GROQ) the same
2052
+ // shape without a Studio plugin — add the plugin later and only this field changes.
2053
+ const featureCardIcon = defineArrayMember({
2054
+ name: "featureCardIcon",
2055
+ type: "object",
2056
+ fields: [
2057
+ defineField({
2058
+ name: "icon",
2059
+ type: "string",
2060
+ description: 'A Lucide icon name, e.g. "zap" or "shield-check".',
2061
+ }),
2062
+ defineField({ name: "title", type: "string" }),
2063
+ defineField({ name: "richText", type: "richText" }),
2064
+ ],
2065
+ preview: { select: { title: "title", subtitle: "icon" } },
2066
+ });
2067
+
2068
+ export const featureCardsIcon = defineType({
2069
+ name: "featureCardsIcon",
2070
+ title: "Feature cards",
2071
+ type: "object",
2072
+ fields: [
2073
+ defineField({ name: "eyebrow", type: "string" }),
2074
+ defineField({ name: "title", type: "string" }),
2075
+ defineField({ name: "richText", type: "richText" }),
2076
+ defineField({ name: "cards", type: "array", of: [featureCardIcon] }),
2077
+ ],
2078
+ preview: {
2079
+ select: { title: "title", cards: "cards" },
2080
+ prepare: ({ title, cards }) => ({
2081
+ title: title ?? "Feature cards",
2082
+ subtitle: \`\${cards?.length ?? 0} cards\`,
2083
+ }),
2084
+ },
2085
+ });
2086
+
2087
+ const imageLinkCard = defineArrayMember({
2088
+ name: "imageLinkCard",
2089
+ type: "object",
2090
+ fields: [
2091
+ defineField({ name: "title", type: "string", validation: (rule) => rule.required() }),
2092
+ defineField({ name: "description", type: "text", rows: 2 }),
2093
+ imageField,
2094
+ defineField({ name: "url", title: "Link", type: "customUrl" }),
2095
+ ],
2096
+ preview: { select: { title: "title", subtitle: "description", media: "image" } },
2097
+ });
2098
+
2099
+ export const imageLinkCards = defineType({
2100
+ name: "imageLinkCards",
2101
+ title: "Image link cards",
2102
+ type: "object",
2103
+ fields: [
2104
+ defineField({ name: "eyebrow", type: "string" }),
2105
+ defineField({ name: "title", type: "string", validation: (rule) => rule.required() }),
2106
+ defineField({ name: "richText", type: "richText" }),
2107
+ buttonsField,
2108
+ defineField({ name: "cards", type: "array", of: [imageLinkCard] }),
2109
+ ],
2110
+ preview: {
2111
+ select: { title: "title", cards: "cards" },
2112
+ prepare: ({ title, cards }) => ({
2113
+ title: title ?? "Image link cards",
2114
+ subtitle: \`\${cards?.length ?? 0} cards\`,
2115
+ }),
2116
+ },
2117
+ });
2118
+
2119
+ export const faqAccordion = defineType({
2120
+ name: "faqAccordion",
2121
+ title: "FAQ accordion",
2122
+ type: "object",
2123
+ fields: [
2124
+ defineField({ name: "eyebrow", type: "string" }),
2125
+ defineField({ name: "title", type: "string", validation: (rule) => rule.required() }),
2126
+ defineField({ name: "subtitle", type: "string" }),
2127
+ defineField({
2128
+ name: "faqs",
2129
+ title: "FAQs",
2130
+ type: "array",
2131
+ of: [defineArrayMember({ type: "reference", to: [{ type: "faq" }] })],
2132
+ validation: (rule) => rule.unique(),
2133
+ }),
2134
+ ],
2135
+ preview: {
2136
+ select: { title: "title", faqs: "faqs" },
2137
+ prepare: ({ title, faqs }) => ({
2138
+ title: title ?? "FAQ accordion",
2139
+ subtitle: \`\${faqs?.length ?? 0} questions\`,
2140
+ }),
2141
+ },
2142
+ });
2143
+
2144
+ export const subscribeNewsletter = defineType({
2145
+ name: "subscribeNewsletter",
2146
+ title: "Subscribe newsletter",
2147
+ type: "object",
2148
+ fields: [
2149
+ defineField({ name: "title", type: "string" }),
2150
+ defineField({ name: "subTitle", title: "Subtitle", type: "richText" }),
2151
+ defineField({ name: "helperText", title: "Helper text", type: "richText" }),
2152
+ ],
2153
+ preview: {
2154
+ select: { title: "title" },
2155
+ prepare: ({ title }) => ({ title: title ?? "Newsletter", subtitle: "Newsletter" }),
2156
+ },
2157
+ });
2158
+
2159
+ /**
2160
+ * Every page-builder block, in the order the \`page\` document offers them. The Studio
2161
+ * imports this and composes it with its own document schemas — one list to keep in step
2162
+ * with the renderers that live beside it, instead of two.
2163
+ */
2164
+ export const blockSchemas = [
2165
+ hero,
2166
+ richTextBlock,
2167
+ featureCardsIcon,
2168
+ imageLinkCards,
2169
+ faqAccordion,
2170
+ subscribeNewsletter,
2171
+ cta,
2172
+ ];
2173
+ `, SCHEMA_PAGE = `import { defineArrayMember, defineField, defineType } from "sanity";
2174
+
2175
+ export const page = defineType({
2176
+ name: "page",
2177
+ title: "Page",
2178
+ type: "document",
2179
+ fields: [
2180
+ defineField({ name: "title", type: "string", validation: (rule) => rule.required() }),
2181
+ defineField({
2182
+ name: "slug",
2183
+ type: "slug",
2184
+ options: { source: "title" },
2185
+ validation: (rule) => rule.required(),
2186
+ }),
2187
+ defineField({ name: "description", type: "text", rows: 3 }),
2188
+ defineField({ name: "image", type: "image", options: { hotspot: true } }),
2189
+ defineField({
2190
+ name: "pageBuilder",
2191
+ title: "Page builder",
2192
+ type: "array",
2193
+ of: [
2194
+ defineArrayMember({ type: "hero" }),
2195
+ defineArrayMember({ type: "richTextBlock" }),
2196
+ defineArrayMember({ type: "featureCardsIcon" }),
2197
+ defineArrayMember({ type: "imageLinkCards" }),
2198
+ defineArrayMember({ type: "faqAccordion" }),
2199
+ defineArrayMember({ type: "subscribeNewsletter" }),
2200
+ defineArrayMember({ type: "cta" }),
2201
+ ],
2202
+ }),
2203
+ ],
2204
+ // The home page is the \`page\` whose slug is "home" — see sanity/queries.ts.
2205
+ preview: { select: { title: "title", subtitle: "slug.current", media: "image" } },
2206
+ });
2207
+ `, SCHEMA_BLOG = `import { defineArrayMember, defineField, defineType } from "sanity";
2208
+
2209
+ export const author = defineType({
2210
+ name: "author",
2211
+ title: "Author",
2212
+ type: "document",
2213
+ fields: [
2214
+ defineField({ name: "name", type: "string", validation: (rule) => rule.required() }),
2215
+ defineField({ name: "position", type: "string" }),
2216
+ defineField({ name: "image", type: "image", options: { hotspot: true } }),
2217
+ defineField({ name: "bio", type: "text", rows: 3 }),
2218
+ ],
2219
+ preview: { select: { title: "name", subtitle: "position", media: "image" } },
2220
+ });
2221
+
2222
+ export const blog = defineType({
2223
+ name: "blog",
2224
+ title: "Blog post",
2225
+ type: "document",
2226
+ fields: [
2227
+ defineField({ name: "title", type: "string", validation: (rule) => rule.required() }),
2228
+ defineField({
2229
+ name: "slug",
2230
+ type: "slug",
2231
+ options: { source: "title" },
2232
+ validation: (rule) => rule.required(),
2233
+ }),
2234
+ defineField({ name: "description", type: "text", rows: 3 }),
2235
+ defineField({
2236
+ name: "authors",
2237
+ type: "array",
2238
+ of: [
2239
+ defineArrayMember({
2240
+ type: "reference",
2241
+ to: [{ type: "author" }],
2242
+ options: { disableNew: true },
2243
+ }),
2244
+ ],
2245
+ validation: (rule) => rule.unique().max(1),
2246
+ }),
2247
+ defineField({
2248
+ name: "publishedAt",
2249
+ title: "Published at",
2250
+ type: "date",
2251
+ // slice, not split("T")[0] — under noUncheckedIndexedAccess the index gives
2252
+ // \`string | undefined\` and initialValue will not take it.
2253
+ initialValue: () => new Date().toISOString().slice(0, 10),
2254
+ }),
2255
+ // Optional, unlike the reference's required image: seeded posts ship without image
2256
+ // assets, and the renderers have to handle a post with no image regardless.
2257
+ defineField({
2258
+ name: "image",
2259
+ type: "image",
2260
+ options: { hotspot: true },
2261
+ fields: [defineField({ name: "alt", title: "Alt text", type: "string" })],
2262
+ }),
2263
+ defineField({ name: "richText", title: "Content", type: "richText" }),
2264
+ ],
2265
+ preview: {
2266
+ select: { title: "title", author: "authors.0.name", date: "publishedAt", media: "image" },
2267
+ prepare: ({ title, author, date, media }) => ({
2268
+ title: title ?? "Untitled post",
2269
+ subtitle: [author, date].filter(Boolean).join(" · "),
2270
+ media,
2271
+ }),
2272
+ },
2273
+ });
2274
+
2275
+ export const faq = defineType({
2276
+ name: "faq",
2277
+ title: "FAQ",
2278
+ type: "document",
2279
+ fields: [
2280
+ defineField({
2281
+ name: "title",
2282
+ title: "Question",
2283
+ type: "string",
2284
+ validation: (rule) => rule.required(),
2285
+ }),
2286
+ defineField({ name: "richText", title: "Answer", type: "richText" }),
2287
+ ],
2288
+ preview: { select: { title: "title" } },
2289
+ });
2290
+ `, SCHEMA_SETTINGS = `import { defineField, defineType } from "sanity";
2291
+
2292
+ export const settings = defineType({
2293
+ name: "settings",
2294
+ title: "Settings",
2295
+ type: "document",
2296
+ fields: [
2297
+ defineField({ name: "siteTitle", type: "string", validation: (rule) => rule.required() }),
2298
+ defineField({ name: "siteDescription", type: "text", rows: 3 }),
2299
+ defineField({
2300
+ name: "socialImage",
2301
+ title: "Social share image",
2302
+ type: "image",
2303
+ options: { hotspot: true },
2304
+ }),
2305
+ ],
2306
+ preview: { select: { title: "siteTitle", media: "socialImage" } },
2307
+ });
2308
+ `, SCHEMA_INDEX = `import { blockSchemas } from "@workspace/sanity-blocks";
2309
+ import type { SchemaTypeDefinition } from "sanity";
2310
+ import { author, blog, faq } from "./blog";
2311
+ import { button, customUrl, richText } from "./definitions";
2312
+ import { page } from "./page";
2313
+ import { settings } from "./settings";
2314
+
2315
+ export const schemaTypes: SchemaTypeDefinition[] = [
2316
+ // Documents
2317
+ page,
2318
+ blog,
2319
+ author,
2320
+ faq,
2321
+ settings,
2322
+ // Reusable definitions
2323
+ customUrl,
2324
+ button,
2325
+ richText,
2326
+ // Page-builder blocks — declared in @workspace/sanity-blocks, next to their renderers.
2327
+ ...blockSchemas,
2328
+ ];
2329
+ `, STRUCTURE = `import type { StructureResolver } from "sanity/structure";
2330
+
2331
+ /**
2332
+ * \`settings\` is a singleton: one document with a fixed id, edited in place instead of
2333
+ * listed. Everything else falls through to the default document-type lists.
2334
+ *
2335
+ * The two sets feed sanity.config.ts's \`document.newDocumentOptions\` / \`document.actions\`
2336
+ * guards (the official singleton recipe), so a second settings document cannot be created.
2337
+ */
2338
+ export const singletonTypes = new Set(["settings"]);
2339
+
2340
+ /** Actions that keep a singleton editable without allowing create/delete/duplicate. */
2341
+ export const singletonActions = new Set(["publish", "discardChanges", "restore"]);
2342
+
2343
+ export const structure: StructureResolver = (S) =>
2344
+ S.list()
2345
+ .title("Content")
2346
+ .items([
2347
+ S.listItem()
2348
+ .title("Settings")
2349
+ .id("settings")
2350
+ .child(S.document().schemaType("settings").documentId("settings")),
2351
+ S.divider(),
2352
+ S.documentTypeListItem("page").title("Pages"),
2353
+ S.documentTypeListItem("blog").title("Blog posts"),
2354
+ S.documentTypeListItem("author").title("Authors"),
2355
+ S.documentTypeListItem("faq").title("FAQs"),
2356
+ ]);
2357
+ `, PRESENTATION = `import { defineDocuments, defineLocations } from "sanity/presentation";
2358
+
2359
+ /** Route → document: lets Presentation open the right document for the URL in the iframe. */
2360
+ export const mainDocuments = defineDocuments([
2361
+ { route: "/", filter: \`_type == "page" && slug.current == "home"\` },
2362
+ { route: "/blog/:slug", filter: \`_type == "blog" && slug.current == $slug\` },
2363
+ { route: "/:slug", filter: \`_type == "page" && slug.current == $slug\` },
2364
+ ]);
2365
+
2366
+ /** Document → routes: the "Used on …" links shown when editing a document. */
2367
+ export const locations = {
2368
+ settings: defineLocations({
2369
+ locations: [{ title: "Home", href: "/" }],
2370
+ message: "This document is used on all pages",
2371
+ tone: "positive",
2372
+ }),
2373
+ page: defineLocations({
2374
+ select: { title: "title", slug: "slug.current" },
2375
+ resolve: (doc) => ({
2376
+ locations: [
2377
+ {
2378
+ title: doc?.title || "Untitled",
2379
+ href: doc?.slug === "home" ? "/" : \`/\${doc?.slug}\`,
2380
+ },
2381
+ ],
2382
+ }),
2383
+ }),
2384
+ blog: defineLocations({
2385
+ select: { title: "title", slug: "slug.current" },
2386
+ resolve: (doc) => ({
2387
+ locations: [
2388
+ { title: doc?.title || "Untitled", href: \`/blog/\${doc?.slug}\` },
2389
+ { title: "Blog", href: "/blog" },
2390
+ ],
2391
+ }),
2392
+ }),
2393
+ };
2394
+ `, DOCUMENT_GUARD = ` document: {
2395
+ newDocumentOptions: (prev, { creationContext }) =>
2396
+ creationContext.type === "global"
2397
+ ? prev.filter((template) => !singletonTypes.has(template.templateId))
2398
+ : prev,
2399
+ actions: (prev, { schemaType }) =>
2400
+ singletonTypes.has(schemaType)
2401
+ ? prev.filter(({ action }) => action && singletonActions.has(action))
2402
+ : prev,
2403
+ },`, QUERIES = `import { defineQuery } from "groq";
2404
+
2405
+ /**
2406
+ * A \`customUrl\` collapsed to a plain href, so components never branch on internal vs
2407
+ * external. Written against a field literally named \`url\` — the shape \`button\` and
2408
+ * \`imageLinkCard\` both use.
2409
+ */
2410
+ const LINK_FIELDS = /* groq */ \`
2411
+ "openInNewTab": url.openInNewTab,
2412
+ "href": select(
2413
+ url.type == "external" => url.external,
2414
+ url.internal->_type == "blog" => "/blog/" + url.internal->slug.current,
2415
+ url.internal->slug.current == "home" => "/",
2416
+ defined(url.internal->slug.current) => "/" + url.internal->slug.current,
2417
+ "#"
2418
+ )
2419
+ \`;
2420
+
2421
+ const BUTTONS = /* groq */ \`buttons[]{ _key, _type, text, variant, \${LINK_FIELDS} }\`;
2422
+
2423
+ // \`...\` first, then the overrides: every block keeps its plain fields, and the two that
2424
+ // hold references or links get resolved on top.
2425
+ const PAGE_BUILDER = /* groq */ \`
2426
+ pageBuilder[]{
2427
+ ...,
2428
+ \${BUTTONS},
2429
+ _type == "faqAccordion" => { faqs[]->{ _id, title, richText } },
2430
+ _type == "imageLinkCards" => { cards[]{ ..., \${LINK_FIELDS} } }
2431
+ }
2432
+ \`;
2433
+
2434
+ const PAGE_FIELDS = /* groq */ \`
2435
+ _id,
2436
+ _type,
2437
+ title,
2438
+ description,
2439
+ "slug": slug.current,
2440
+ \${PAGE_BUILDER}
2441
+ \`;
2442
+
2443
+ const BLOG_CARD_FIELDS = /* groq */ \`
2444
+ _id,
2445
+ title,
2446
+ description,
2447
+ publishedAt,
2448
+ image,
2449
+ "slug": slug.current,
2450
+ "authors": authors[]->{ _id, name, position, image }
2451
+ \`;
2452
+
2453
+ export const HOME_PAGE_QUERY = defineQuery(
2454
+ \`*[_type == "page" && slug.current == "home"][0]{\${PAGE_FIELDS}}\`,
2455
+ );
2456
+
2457
+ export const PAGE_QUERY = defineQuery(
2458
+ \`*[_type == "page" && slug.current == $slug][0]{\${PAGE_FIELDS}}\`,
2459
+ );
2460
+
2461
+ export const PAGE_SLUGS_QUERY = defineQuery(
2462
+ \`*[_type == "page" && defined(slug.current) && slug.current != "home"].slug.current\`,
2463
+ );
2464
+
2465
+ export const BLOG_INDEX_QUERY = defineQuery(
2466
+ \`*[_type == "blog" && defined(slug.current)]|order(publishedAt desc){\${BLOG_CARD_FIELDS}}\`,
2467
+ );
2468
+
2469
+ export const BLOG_QUERY = defineQuery(
2470
+ \`*[_type == "blog" && slug.current == $slug][0]{\${BLOG_CARD_FIELDS}, richText}\`,
2471
+ );
2472
+
2473
+ export const BLOG_SLUGS_QUERY = defineQuery(
2474
+ \`*[_type == "blog" && defined(slug.current)].slug.current\`,
2475
+ );
2476
+
2477
+ export const SETTINGS_QUERY = defineQuery(
2478
+ \`*[_type == "settings"][0]{ siteTitle, siteDescription, socialImage }\`,
2479
+ );
2480
+ `;
2481
+
2482
+ // ../../packages/registry/src/content/sanity/seed.ts
2483
+ var para = (key, text) => ({
2484
+ _key: key,
2485
+ _type: "block",
2486
+ style: "normal",
2487
+ markDefs: [],
2488
+ children: [{ _key: `${key}s`, _type: "span", marks: [], text }]
2489
+ }), internal = (id) => ({
2490
+ _type: "customUrl",
2491
+ type: "internal",
2492
+ openInNewTab: false,
2493
+ internal: { _type: "reference", _ref: id }
2494
+ }), external = (href) => ({
2495
+ _type: "customUrl",
2496
+ type: "external",
2497
+ openInNewTab: true,
2498
+ external: href
2499
+ }), button = (key, text, url, variant = "default") => ({
2500
+ _key: key,
2501
+ _type: "button",
2502
+ text,
2503
+ variant,
2504
+ url
2505
+ }), DOCS, SEED_NDJSON;
2506
+ var init_seed = __esm(() => {
2507
+ DOCS = [
2508
+ {
2509
+ _id: "settings",
2510
+ _type: "settings",
2511
+ siteTitle: "Turbo Wizard",
2512
+ siteDescription: "A Turborepo monorepo scaffolded with create-turbo-wizard, with Sanity wired end to end."
2513
+ },
2514
+ {
2515
+ _id: "author-nova",
2516
+ _type: "author",
2517
+ name: "Nova Lang",
2518
+ position: "Staff engineer",
2519
+ bio: "Writes about build systems and the parts of a monorepo nobody reads until they break."
2520
+ },
2521
+ {
2522
+ _id: "author-atlas",
2523
+ _type: "author",
2524
+ name: "Atlas Reyes",
2525
+ position: "Content lead",
2526
+ bio: "Turns a schema into something an editor can actually use."
2527
+ },
2528
+ {
2529
+ _id: "faq-content",
2530
+ _type: "faq",
2531
+ title: "Where does this content live?",
2532
+ richText: [
2533
+ 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.")
2534
+ ]
2535
+ },
2536
+ {
2537
+ _id: "faq-blocks",
2538
+ _type: "faq",
2539
+ title: "How do I add a page-builder block?",
2540
+ richText: [
2541
+ 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.")
2542
+ ]
2543
+ },
2544
+ {
2545
+ _id: "faq-credentials",
2546
+ _type: "faq",
2547
+ title: "What happens without a project id?",
2548
+ richText: [
2549
+ 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.")
2550
+ ]
2551
+ },
2552
+ {
2553
+ _id: "page-home",
2554
+ _type: "page",
2555
+ title: "Turbo Wizard",
2556
+ description: "The home page, served from Sanity.",
2557
+ slug: { _type: "slug", current: "home" },
2558
+ pageBuilder: [
2559
+ {
2560
+ _key: "hero",
2561
+ _type: "hero",
2562
+ badge: "Scaffolded",
2563
+ title: "Content from Sanity, rendered by your app",
2564
+ richText: [
2565
+ para("h1", "This page lives in the Studio. Edit it there and it changes here — every block below came out of the same dataset.")
2566
+ ],
2567
+ buttons: [
2568
+ button("hb1", "Read the blog", internal("blog-page-builder")),
2569
+ button("hb2", "Sanity docs", external("https://www.sanity.io/docs"), "outline")
2570
+ ]
2571
+ },
2572
+ {
2573
+ _key: "features",
2574
+ _type: "featureCardsIcon",
2575
+ eyebrow: "What you got",
2576
+ title: "Wired end to end",
2577
+ cards: [
2578
+ {
2579
+ _key: "f1",
2580
+ _type: "featureCardIcon",
2581
+ icon: "layers",
2582
+ title: "Page builder",
2583
+ richText: [para("f1a", "Seven block types, composed per page in the Studio.")]
2584
+ },
2585
+ {
2586
+ _key: "f2",
2587
+ _type: "featureCardIcon",
2588
+ icon: "pen-line",
2589
+ title: "Blog",
2590
+ richText: [para("f2a", "Posts with authors, dates and Portable Text bodies.")]
2591
+ },
2592
+ {
2593
+ _key: "f3",
2594
+ _type: "featureCardIcon",
2595
+ icon: "shield-check",
2596
+ title: "Typed queries",
2597
+ richText: [para("f3a", "defineQuery plus typegen, so results are not any.")]
2598
+ }
2599
+ ]
2600
+ },
2601
+ {
2602
+ _key: "cards",
2603
+ _type: "imageLinkCards",
2604
+ eyebrow: "Go deeper",
2605
+ title: "Where to look next",
2606
+ cards: [
2607
+ {
2608
+ _key: "c1",
2609
+ _type: "imageLinkCard",
2610
+ title: "The schema",
2611
+ description: "Documents, blocks and the reusable definitions they share.",
2612
+ url: external("https://www.sanity.io/docs/schema-types")
2613
+ },
2614
+ {
2615
+ _key: "c2",
2616
+ _type: "imageLinkCard",
2617
+ title: "About this starter",
2618
+ description: "A second page, routed by its slug.",
2619
+ url: internal("page-about")
2620
+ }
2621
+ ]
2622
+ },
2623
+ {
2624
+ _key: "faqs",
2625
+ _type: "faqAccordion",
2626
+ eyebrow: "Questions",
2627
+ title: "The ones that come up first",
2628
+ faqs: [
2629
+ { _key: "q1", _type: "reference", _ref: "faq-content" },
2630
+ { _key: "q2", _type: "reference", _ref: "faq-blocks" },
2631
+ { _key: "q3", _type: "reference", _ref: "faq-credentials" }
2632
+ ]
2633
+ },
2634
+ {
2635
+ _key: "prose",
2636
+ _type: "richTextBlock",
2637
+ eyebrow: "How it fits together",
2638
+ title: "One schema, two frontends",
2639
+ richText: [
2640
+ 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."),
2641
+ 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.")
2642
+ ]
2643
+ },
2644
+ {
2645
+ _key: "newsletter",
2646
+ _type: "subscribeNewsletter",
2647
+ title: "Ship notes",
2648
+ subTitle: [para("n1", "Occasional posts about the stack this project scaffolds.")],
2649
+ helperText: [para("n2", "No spam. The form is a stub until you wire it up.")]
2650
+ },
2651
+ {
2652
+ _key: "cta",
2653
+ _type: "cta",
2654
+ eyebrow: "Your turn",
2655
+ title: "Open the Studio",
2656
+ richText: [
2657
+ para("ct1", "Edit this page in the Studio — pnpm dev:studio — then publish and reload.")
2658
+ ],
2659
+ buttons: [button("cb1", "Read the blog", internal("blog-typed-groq"))]
2660
+ }
2661
+ ]
2662
+ },
2663
+ {
2664
+ _id: "page-about",
2665
+ _type: "page",
2666
+ title: "About",
2667
+ description: "A second page, to show routing by slug.",
2668
+ slug: { _type: "slug", current: "about" },
2669
+ pageBuilder: [
2670
+ {
2671
+ _key: "hero",
2672
+ _type: "hero",
2673
+ title: "About this starter",
2674
+ richText: [para("ab1", "Pages are routed by their slug. This one is /about.")]
2675
+ },
2676
+ {
2677
+ _key: "prose",
2678
+ _type: "richTextBlock",
2679
+ title: "Add your own",
2680
+ richText: [
2681
+ para("ab2", "Create a page in the Studio, give it a slug, publish it, and it appears at that path on the next build.")
2682
+ ]
2683
+ }
2684
+ ]
2685
+ },
2686
+ {
2687
+ _id: "blog-page-builder",
2688
+ _type: "blog",
2689
+ title: "A page builder is just an array",
2690
+ description: "The page-builder pattern in one sentence: a document holds an array of objects, and the frontend renders each one by its _type.",
2691
+ slug: { _type: "slug", current: "a-page-builder-is-just-an-array" },
2692
+ publishedAt: "2026-01-14",
2693
+ authors: [{ _key: "a1", _type: "reference", _ref: "author-nova" }],
2694
+ richText: [
2695
+ 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."),
2696
+ 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.")
2697
+ ]
2698
+ },
2699
+ {
2700
+ _id: "blog-typed-groq",
2701
+ _type: "blog",
2702
+ title: "Typed GROQ, without hand-writing types",
2703
+ description: "defineQuery marks a string as a query, sanity typegen reads the schema, and query results stop being any.",
2704
+ slug: { _type: "slug", current: "typed-groq" },
2705
+ publishedAt: "2026-01-28",
2706
+ authors: [{ _key: "a1", _type: "reference", _ref: "author-nova" }],
2707
+ richText: [
2708
+ 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."),
2709
+ 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.")
2710
+ ]
2711
+ },
2712
+ {
2713
+ _id: "blog-editors",
2714
+ _type: "blog",
2715
+ title: "Write the schema for the person editing it",
2716
+ description: "Field descriptions, previews and singletons are not polish. They are the difference between a Studio someone uses and one they avoid.",
2717
+ slug: { _type: "slug", current: "write-the-schema-for-the-editor" },
2718
+ publishedAt: "2026-02-09",
2719
+ authors: [{ _key: "a1", _type: "reference", _ref: "author-atlas" }],
2720
+ richText: [
2721
+ 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."),
2722
+ 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.")
2723
+ ]
2724
+ }
2725
+ ];
2726
+ SEED_NDJSON = `${DOCS.map((doc) => JSON.stringify(doc)).join(`
2727
+ `)}
2728
+ `;
2729
+ });
2730
+
2731
+ // ../../packages/registry/src/content/sanity/studio.ts
2732
+ var j5 = (value) => `${JSON.stringify(value, null, 2)}
2733
+ `, 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.
2734
+ # A Studio only reads SANITY_STUDIO_*-prefixed variables.
2735
+ SANITY_STUDIO_PROJECT_ID=
2736
+ SANITY_STUDIO_DATASET=
2737
+ # Where Presentation points its preview iframe (the site, not the Studio).
2738
+ SANITY_STUDIO_PRESENTATION_URL=${siteUrl}
2739
+ `, STUDIO_ENV_EXAMPLE_NEXT, STUDIO_ENV_EXAMPLE_ASTRO, STUDIO_CLI = `import { defineCliConfig } from "sanity/cli";
2740
+
2741
+ export default defineCliConfig({
2742
+ api: {
2743
+ projectId: process.env.SANITY_STUDIO_PROJECT_ID || "placeholder",
2744
+ dataset: process.env.SANITY_STUDIO_DATASET || "production",
2745
+ },
2746
+ // The Studio owns extraction and typegen — the CLI has to run where sanity.config.ts
2747
+ // lives — but the generated types land beside the queries they type, in
2748
+ // @workspace/sanity. Paths are relative to apps/studio; \`schema\` defaults to the
2749
+ // schema.json that \`sanity schema extract\` writes here.
2750
+ typegen: {
2751
+ path: "../../packages/sanity/src/**/*.{ts,tsx}",
2752
+ generates: "../../packages/sanity/src/sanity.types.ts",
2753
+ },
2754
+ });
2755
+ `, studioConfig = (siteUrl, previewMode) => `import { defineConfig } from "sanity";
2756
+ import { presentationTool } from "sanity/presentation";
2757
+ import { structureTool } from "sanity/structure";
2758
+ import { locations, mainDocuments } from "./presentation";
2759
+ import { schemaTypes } from "./schemaTypes/index";
2760
+ import { singletonActions, singletonTypes, structure } from "./structure";
2761
+
2762
+ export default defineConfig({
2763
+ name: "default",
2764
+ title: "Studio",
2765
+ projectId: process.env.SANITY_STUDIO_PROJECT_ID || "placeholder",
2766
+ dataset: process.env.SANITY_STUDIO_DATASET || "production",
2767
+ plugins: [
2768
+ presentationTool({
2769
+ resolve: { mainDocuments, locations },
2770
+ previewUrl: {
2771
+ // Absolute, unlike an embedded Studio's "/": the site is a different origin now.
2772
+ initial: process.env.SANITY_STUDIO_PRESENTATION_URL || "${siteUrl}",${previewMode}
2773
+ },
2774
+ }),
2775
+ structureTool({ structure }),
2776
+ ],
2777
+ schema: { types: schemaTypes },
2778
+ ${DOCUMENT_GUARD}
2779
+ });
2780
+ `, STUDIO_CONFIG_NEXT, STUDIO_CONFIG_ASTRO;
2781
+ var init_studio = __esm(() => {
2782
+ STUDIO_PACKAGE_JSON = j5({
2783
+ name: "studio",
2784
+ version: "0.0.0",
2785
+ private: true,
2786
+ type: "module",
2787
+ scripts: {
2788
+ build: "sanity build",
2789
+ "check-types": "tsc --noEmit",
2790
+ dev: "sanity dev",
2791
+ extract: "sanity schema extract --enforce-required-fields",
2792
+ "sanity:seed": "sanity dataset import seed.ndjson",
2793
+ type: "sanity typegen generate"
2794
+ },
2795
+ dependencies: {
2796
+ "@workspace/sanity-blocks": "workspace:*",
2797
+ react: "^19.0.0",
2798
+ "react-dom": "^19.0.0",
2799
+ sanity: "^5.31.1",
2800
+ "styled-components": "^6.1.15"
2801
+ },
2802
+ devDependencies: {
2803
+ "@types/node": "^22.10.0",
2804
+ "@types/react": "^19.0.0",
2805
+ "@workspace/typescript-config": "workspace:*",
2806
+ typescript: "^5.9.0"
2807
+ }
2808
+ });
2809
+ STUDIO_TSCONFIG = j5({
2810
+ extends: "@workspace/typescript-config/react-library.json",
2811
+ include: ["**/*.ts", "**/*.tsx"],
2812
+ exclude: ["node_modules", "dist"]
2813
+ });
2814
+ STUDIO_ENV_EXAMPLE_NEXT = studioEnvExample(NEXT_SITE_URL);
2815
+ STUDIO_ENV_EXAMPLE_ASTRO = studioEnvExample(ASTRO_SITE_URL);
2816
+ STUDIO_CONFIG_NEXT = studioConfig(NEXT_SITE_URL, `
2817
+ previewMode: {
2818
+ enable: "/api/draft-mode/enable",
2819
+ // Presentation never calls this itself (documented as not yet implemented);
2820
+ // editors leave draft mode via <DisableDraftMode/>, which links to the route.
2821
+ disable: "/api/draft-mode/disable",
2822
+ },`);
2823
+ STUDIO_CONFIG_ASTRO = studioConfig(ASTRO_SITE_URL, `
2824
+ // No previewMode: Astro has no draft-mode routes. Draft content comes from
2825
+ // loadQuery's perspective switch (PUBLIC_SANITY_VISUAL_EDITING_ENABLED + a read
2826
+ // token), which needs a rendering server — \`astro dev\`, or an SSR adapter.`);
2827
+ });
2828
+
2829
+ // ../../packages/registry/src/content/sanity/types.ts
2830
+ var SANITY_TYPES = `/**
2831
+ * ---------------------------------------------------------------------------------
2832
+ * This file has been generated by Sanity TypeGen.
2833
+ * Command: \`sanity typegen generate\`
2834
+ *
2835
+ * Any modifications made directly to this file will be overwritten the next time
2836
+ * the TypeScript definitions are generated. Please make changes to the Sanity
2837
+ * schema definitions and/or GROQ queries if you need to update these types.
2838
+ *
2839
+ * For more information on how to use Sanity TypeGen, visit the official documentation:
2840
+ * https://www.sanity.io/docs/sanity-typegen
2841
+ * ---------------------------------------------------------------------------------
1392
2842
  */
1393
- export async function loadQuery<T>(
1394
- query: string,
1395
- params: Record<string, unknown> = {},
1396
- ): Promise<T | null> {
1397
- if (!hasSanityCredentials) return null;
1398
- if (visualEditingEnabled && !readToken) {
1399
- throw new Error(
1400
- "SANITY_API_READ_TOKEN is required when PUBLIC_SANITY_VISUAL_EDITING_ENABLED is true.",
1401
- );
1402
- }
1403
- try {
1404
- const { result } = await sanityClient.fetch<T>(query, params, {
1405
- filterResponse: false,
1406
- perspective: visualEditingEnabled ? "drafts" : "published",
1407
- resultSourceMap: visualEditingEnabled ? "withKeyArraySelector" : false,
1408
- stega: visualEditingEnabled,
1409
- ...(visualEditingEnabled ? { token: readToken } : {}),
1410
- useCdn: !visualEditingEnabled,
1411
- });
1412
- return result;
1413
- } catch (error) {
1414
- console.warn("[sanity] query failed:", error);
1415
- return null;
1416
- }
1417
- }
1418
- `, ASTRO_IMAGE = `import { createImageUrlBuilder } from "@sanity/image-url";
1419
- import { dataset, projectId } from "./env";
1420
2843
 
1421
- const builder = createImageUrlBuilder({ projectId, dataset });
2844
+ export declare const internalGroqTypeReferenceTo: unique symbol;
1422
2845
 
1423
- /** Build a CDN URL for a Sanity image; null when the block has no asset. */
1424
- export function urlFor(source: unknown, width = 1200): string | null {
1425
- const ref = (source as { asset?: { _ref?: string } } | null)?.asset?._ref;
1426
- if (!ref) return null;
1427
- return builder.image(ref).width(width).auto("format").url();
1428
- }
1429
- `, ASTRO_TYPES = `import type { PortableTextBlock } from "@portabletext/types";
2846
+ // Source: schema.json
2847
+ export type SanityImageAssetReference = {
2848
+ _ref: string;
2849
+ _type: "reference";
2850
+ _weak?: boolean;
2851
+ [internalGroqTypeReferenceTo]?: "sanity.imageAsset";
2852
+ };
1430
2853
 
1431
- type SanityImage = { asset?: { _ref?: string } | null; alt?: string | null } | null;
2854
+ export type Image1 = {
2855
+ asset?: SanityImageAssetReference;
2856
+ media?: unknown; // Unable to locate the referenced type "image.media" in schema
2857
+ hotspot?: SanityImageHotspot;
2858
+ crop?: SanityImageCrop;
2859
+ alt?: string;
2860
+ _type: "image";
2861
+ };
1432
2862
 
1433
- export type PageBlock =
1434
- | { _key: string; _type: "hero"; heading?: string; subheading?: string; image?: SanityImage }
1435
- | { _key: string; _type: "richText"; heading?: string; content?: PortableTextBlock[] }
2863
+ export type Cta = {
2864
+ _type: "cta";
2865
+ eyebrow?: string;
2866
+ title?: string;
2867
+ richText?: RichText;
2868
+ buttons?: Array<
2869
+ {
2870
+ _key: string;
2871
+ } & Button
2872
+ >;
2873
+ };
2874
+
2875
+ export type SubscribeNewsletter = {
2876
+ _type: "subscribeNewsletter";
2877
+ title?: string;
2878
+ subTitle?: RichText;
2879
+ helperText?: RichText;
2880
+ };
2881
+
2882
+ export type FaqReference = {
2883
+ _ref: string;
2884
+ _type: "reference";
2885
+ _weak?: boolean;
2886
+ [internalGroqTypeReferenceTo]?: "faq";
2887
+ };
2888
+
2889
+ export type FaqAccordion = {
2890
+ _type: "faqAccordion";
2891
+ eyebrow?: string;
2892
+ title: string;
2893
+ subtitle?: string;
2894
+ faqs?: Array<
2895
+ {
2896
+ _key: string;
2897
+ } & FaqReference
2898
+ >;
2899
+ };
2900
+
2901
+ export type ImageLinkCards = {
2902
+ _type: "imageLinkCards";
2903
+ eyebrow?: string;
2904
+ title: string;
2905
+ richText?: RichText;
2906
+ buttons?: Array<
2907
+ {
2908
+ _key: string;
2909
+ } & Button
2910
+ >;
2911
+ cards?: Array<{
2912
+ title: string;
2913
+ description?: string;
2914
+ image?: Image1;
2915
+ url?: CustomUrl;
2916
+ _type: "imageLinkCard";
2917
+ _key: string;
2918
+ }>;
2919
+ };
2920
+
2921
+ export type FeatureCardsIcon = {
2922
+ _type: "featureCardsIcon";
2923
+ eyebrow?: string;
2924
+ title?: string;
2925
+ richText?: RichText;
2926
+ cards?: Array<{
2927
+ icon?: string;
2928
+ title?: string;
2929
+ richText?: RichText;
2930
+ _type: "featureCardIcon";
2931
+ _key: string;
2932
+ }>;
2933
+ };
2934
+
2935
+ export type RichTextBlock = {
2936
+ _type: "richTextBlock";
2937
+ eyebrow?: string;
2938
+ title?: string;
2939
+ richText?: RichText;
2940
+ };
2941
+
2942
+ export type Hero = {
2943
+ _type: "hero";
2944
+ badge?: string;
2945
+ title?: string;
2946
+ richText?: RichText;
2947
+ image?: Image1;
2948
+ buttons?: Array<
2949
+ {
2950
+ _key: string;
2951
+ } & Button
2952
+ >;
2953
+ };
2954
+
2955
+ export type RichText = Array<
1436
2956
  | {
2957
+ children?: Array<{
2958
+ marks?: Array<string>;
2959
+ text?: string;
2960
+ _type: "span";
2961
+ _key: string;
2962
+ }>;
2963
+ style?: "normal" | "h2" | "h3" | "h4";
2964
+ listItem?: "bullet" | "number";
2965
+ markDefs?: Array<{
2966
+ customLink?: CustomUrl;
2967
+ _type: "customLink";
2968
+ _key: string;
2969
+ }>;
2970
+ level?: number;
2971
+ _type: "block";
1437
2972
  _key: string;
1438
- _type: "cta";
1439
- heading?: string;
1440
- text?: string;
1441
- buttonLabel?: string;
1442
- buttonHref?: string;
1443
- };
2973
+ }
2974
+ | {
2975
+ asset?: SanityImageAssetReference;
2976
+ media?: unknown;
2977
+ hotspot?: SanityImageHotspot;
2978
+ crop?: SanityImageCrop;
2979
+ caption?: string;
2980
+ _type: "image";
2981
+ _key: string;
2982
+ }
2983
+ >;
2984
+
2985
+ export type Button = {
2986
+ _type: "button";
2987
+ variant?: "default" | "secondary" | "outline" | "link";
2988
+ text?: string;
2989
+ url?: CustomUrl;
2990
+ };
2991
+
2992
+ export type PageReference = {
2993
+ _ref: string;
2994
+ _type: "reference";
2995
+ _weak?: boolean;
2996
+ [internalGroqTypeReferenceTo]?: "page";
2997
+ };
2998
+
2999
+ export type BlogReference = {
3000
+ _ref: string;
3001
+ _type: "reference";
3002
+ _weak?: boolean;
3003
+ [internalGroqTypeReferenceTo]?: "blog";
3004
+ };
3005
+
3006
+ export type CustomUrl = {
3007
+ _type: "customUrl";
3008
+ type: "internal" | "external";
3009
+ openInNewTab?: boolean;
3010
+ external?: string;
3011
+ internal?: PageReference | BlogReference;
3012
+ };
3013
+
3014
+ export type Settings = {
3015
+ _id: string;
3016
+ _type: "settings";
3017
+ _createdAt: string;
3018
+ _updatedAt: string;
3019
+ _rev: string;
3020
+ siteTitle: string;
3021
+ siteDescription?: string;
3022
+ socialImage?: {
3023
+ asset?: SanityImageAssetReference;
3024
+ media?: unknown;
3025
+ hotspot?: SanityImageHotspot;
3026
+ crop?: SanityImageCrop;
3027
+ _type: "image";
3028
+ };
3029
+ };
3030
+
3031
+ export type SanityImageCrop = {
3032
+ _type: "sanity.imageCrop";
3033
+ top: number;
3034
+ bottom: number;
3035
+ left: number;
3036
+ right: number;
3037
+ };
3038
+
3039
+ export type SanityImageHotspot = {
3040
+ _type: "sanity.imageHotspot";
3041
+ x: number;
3042
+ y: number;
3043
+ height: number;
3044
+ width: number;
3045
+ };
1444
3046
 
1445
- export type SanityPage = {
3047
+ export type Faq = {
1446
3048
  _id: string;
3049
+ _type: "faq";
3050
+ _createdAt: string;
3051
+ _updatedAt: string;
3052
+ _rev: string;
3053
+ title: string;
3054
+ richText?: RichText;
3055
+ };
3056
+
3057
+ export type Author = {
3058
+ _id: string;
3059
+ _type: "author";
3060
+ _createdAt: string;
3061
+ _updatedAt: string;
3062
+ _rev: string;
3063
+ name: string;
3064
+ position?: string;
3065
+ image?: {
3066
+ asset?: SanityImageAssetReference;
3067
+ media?: unknown;
3068
+ hotspot?: SanityImageHotspot;
3069
+ crop?: SanityImageCrop;
3070
+ _type: "image";
3071
+ };
3072
+ bio?: string;
3073
+ };
3074
+
3075
+ export type AuthorReference = {
3076
+ _ref: string;
3077
+ _type: "reference";
3078
+ _weak?: boolean;
3079
+ [internalGroqTypeReferenceTo]?: "author";
3080
+ };
3081
+
3082
+ export type Blog = {
3083
+ _id: string;
3084
+ _type: "blog";
3085
+ _createdAt: string;
3086
+ _updatedAt: string;
3087
+ _rev: string;
3088
+ title: string;
3089
+ slug: Slug;
3090
+ description?: string;
3091
+ authors?: Array<
3092
+ {
3093
+ _key: string;
3094
+ } & AuthorReference
3095
+ >;
3096
+ publishedAt?: string;
3097
+ image?: {
3098
+ asset?: SanityImageAssetReference;
3099
+ media?: unknown;
3100
+ hotspot?: SanityImageHotspot;
3101
+ crop?: SanityImageCrop;
3102
+ alt?: string;
3103
+ _type: "image";
3104
+ };
3105
+ richText?: RichText;
3106
+ };
3107
+
3108
+ export type Slug = {
3109
+ _type: "slug";
3110
+ current: string;
3111
+ source?: string;
3112
+ };
3113
+
3114
+ export type Page = {
3115
+ _id: string;
3116
+ _type: "page";
3117
+ _createdAt: string;
3118
+ _updatedAt: string;
3119
+ _rev: string;
3120
+ title: string;
3121
+ slug: Slug;
3122
+ description?: string;
3123
+ image?: {
3124
+ asset?: SanityImageAssetReference;
3125
+ media?: unknown;
3126
+ hotspot?: SanityImageHotspot;
3127
+ crop?: SanityImageCrop;
3128
+ _type: "image";
3129
+ };
3130
+ pageBuilder?: Array<
3131
+ | ({
3132
+ _key: string;
3133
+ } & Hero)
3134
+ | ({
3135
+ _key: string;
3136
+ } & RichTextBlock)
3137
+ | ({
3138
+ _key: string;
3139
+ } & FeatureCardsIcon)
3140
+ | ({
3141
+ _key: string;
3142
+ } & ImageLinkCards)
3143
+ | ({
3144
+ _key: string;
3145
+ } & FaqAccordion)
3146
+ | ({
3147
+ _key: string;
3148
+ } & SubscribeNewsletter)
3149
+ | ({
3150
+ _key: string;
3151
+ } & Cta)
3152
+ >;
3153
+ };
3154
+
3155
+ export type SanityImagePaletteSwatch = {
3156
+ _type: "sanity.imagePaletteSwatch";
3157
+ background?: string;
3158
+ foreground?: string;
3159
+ population?: number;
3160
+ title?: string;
3161
+ };
3162
+
3163
+ export type SanityImagePalette = {
3164
+ _type: "sanity.imagePalette";
3165
+ darkMuted?: SanityImagePaletteSwatch;
3166
+ lightVibrant?: SanityImagePaletteSwatch;
3167
+ darkVibrant?: SanityImagePaletteSwatch;
3168
+ vibrant?: SanityImagePaletteSwatch;
3169
+ dominant?: SanityImagePaletteSwatch;
3170
+ lightMuted?: SanityImagePaletteSwatch;
3171
+ muted?: SanityImagePaletteSwatch;
3172
+ };
3173
+
3174
+ export type SanityImageDimensions = {
3175
+ _type: "sanity.imageDimensions";
3176
+ height: number;
3177
+ width: number;
3178
+ aspectRatio: number;
3179
+ };
3180
+
3181
+ export type SanityImageMetadata = {
3182
+ _type: "sanity.imageMetadata";
3183
+ location?: Geopoint;
3184
+ dimensions?: SanityImageDimensions;
3185
+ palette?: SanityImagePalette;
3186
+ lqip?: string;
3187
+ blurHash?: string;
3188
+ thumbHash?: string;
3189
+ hasAlpha?: boolean;
3190
+ isOpaque?: boolean;
3191
+ };
3192
+
3193
+ export type SanityFileAsset = {
3194
+ _id: string;
3195
+ _type: "sanity.fileAsset";
3196
+ _createdAt: string;
3197
+ _updatedAt: string;
3198
+ _rev: string;
3199
+ originalFilename?: string;
3200
+ label?: string;
3201
+ title?: string;
3202
+ description?: string;
3203
+ altText?: string;
3204
+ sha1hash: string;
3205
+ extension: string;
3206
+ mimeType: string;
3207
+ size: number;
3208
+ assetId: string;
3209
+ uploadId?: string;
3210
+ path: string;
3211
+ url: string;
3212
+ source?: SanityAssetSourceData;
3213
+ };
3214
+
3215
+ export type SanityAssetSourceData = {
3216
+ _type: "sanity.assetSourceData";
3217
+ name?: string;
3218
+ id?: string;
3219
+ url?: string;
3220
+ };
3221
+
3222
+ export type SanityImageAsset = {
3223
+ _id: string;
3224
+ _type: "sanity.imageAsset";
3225
+ _createdAt: string;
3226
+ _updatedAt: string;
3227
+ _rev: string;
3228
+ originalFilename?: string;
3229
+ label?: string;
1447
3230
  title?: string;
1448
3231
  description?: string;
1449
- slug?: string;
1450
- pageBuilder?: PageBlock[] | null;
3232
+ altText?: string;
3233
+ sha1hash: string;
3234
+ extension: string;
3235
+ mimeType: string;
3236
+ size: number;
3237
+ assetId: string;
3238
+ uploadId?: string;
3239
+ path: string;
3240
+ url: string;
3241
+ metadata?: SanityImageMetadata;
3242
+ source?: SanityAssetSourceData;
3243
+ };
3244
+
3245
+ export type Geopoint = {
3246
+ _type: "geopoint";
3247
+ lat?: number;
3248
+ lng?: number;
3249
+ alt?: number;
3250
+ };
3251
+
3252
+ export type AllSanitySchemaTypes =
3253
+ | SanityImageAssetReference
3254
+ | Image1
3255
+ | Cta
3256
+ | SubscribeNewsletter
3257
+ | FaqReference
3258
+ | FaqAccordion
3259
+ | ImageLinkCards
3260
+ | FeatureCardsIcon
3261
+ | RichTextBlock
3262
+ | Hero
3263
+ | RichText
3264
+ | Button
3265
+ | PageReference
3266
+ | BlogReference
3267
+ | CustomUrl
3268
+ | Settings
3269
+ | SanityImageCrop
3270
+ | SanityImageHotspot
3271
+ | Faq
3272
+ | Author
3273
+ | AuthorReference
3274
+ | Blog
3275
+ | Slug
3276
+ | Page
3277
+ | SanityImagePaletteSwatch
3278
+ | SanityImagePalette
3279
+ | SanityImageDimensions
3280
+ | SanityImageMetadata
3281
+ | SanityFileAsset
3282
+ | SanityAssetSourceData
3283
+ | SanityImageAsset
3284
+ | Geopoint;
3285
+
3286
+ // Source: ../../packages/sanity/src/queries.ts
3287
+ // Variable: HOME_PAGE_QUERY
3288
+ // 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, "#" ) } } }}
3289
+ export type HOME_PAGE_QUERY_RESULT = {
3290
+ _id: string;
3291
+ _type: "page";
3292
+ title: string;
3293
+ description: string | null;
3294
+ slug: string;
3295
+ pageBuilder: Array<
3296
+ | {
3297
+ _key: string;
3298
+ _type: "cta";
3299
+ eyebrow?: string;
3300
+ title?: string;
3301
+ richText?: RichText;
3302
+ buttons: Array<{
3303
+ _key: string;
3304
+ _type: "button";
3305
+ text: string | null;
3306
+ variant: "default" | "link" | "outline" | "secondary" | null;
3307
+ openInNewTab: boolean | null;
3308
+ href: string | "/" | "#" | null;
3309
+ }> | null;
3310
+ }
3311
+ | {
3312
+ _key: string;
3313
+ _type: "faqAccordion";
3314
+ eyebrow?: string;
3315
+ title: string;
3316
+ subtitle?: string;
3317
+ faqs: Array<{
3318
+ _id: string;
3319
+ title: string;
3320
+ richText: RichText | null;
3321
+ }> | null;
3322
+ buttons: null;
3323
+ }
3324
+ | {
3325
+ _key: string;
3326
+ _type: "featureCardsIcon";
3327
+ eyebrow?: string;
3328
+ title?: string;
3329
+ richText?: RichText;
3330
+ cards?: Array<{
3331
+ icon?: string;
3332
+ title?: string;
3333
+ richText?: RichText;
3334
+ _type: "featureCardIcon";
3335
+ _key: string;
3336
+ }>;
3337
+ buttons: null;
3338
+ }
3339
+ | {
3340
+ _key: string;
3341
+ _type: "hero";
3342
+ badge?: string;
3343
+ title?: string;
3344
+ richText?: RichText;
3345
+ image?: Image1;
3346
+ buttons: Array<{
3347
+ _key: string;
3348
+ _type: "button";
3349
+ text: string | null;
3350
+ variant: "default" | "link" | "outline" | "secondary" | null;
3351
+ openInNewTab: boolean | null;
3352
+ href: string | "/" | "#" | null;
3353
+ }> | null;
3354
+ }
3355
+ | {
3356
+ _key: string;
3357
+ _type: "imageLinkCards";
3358
+ eyebrow?: string;
3359
+ title: string;
3360
+ richText?: RichText;
3361
+ buttons: Array<{
3362
+ _key: string;
3363
+ _type: "button";
3364
+ text: string | null;
3365
+ variant: "default" | "link" | "outline" | "secondary" | null;
3366
+ openInNewTab: boolean | null;
3367
+ href: string | "/" | "#" | null;
3368
+ }> | null;
3369
+ cards: Array<{
3370
+ title: string;
3371
+ description?: string;
3372
+ image?: Image1;
3373
+ url?: CustomUrl;
3374
+ _type: "imageLinkCard";
3375
+ _key: string;
3376
+ openInNewTab: boolean | null;
3377
+ href: string | "/" | "#" | null;
3378
+ }> | null;
3379
+ }
3380
+ | {
3381
+ _key: string;
3382
+ _type: "richTextBlock";
3383
+ eyebrow?: string;
3384
+ title?: string;
3385
+ richText?: RichText;
3386
+ buttons: null;
3387
+ }
3388
+ | {
3389
+ _key: string;
3390
+ _type: "subscribeNewsletter";
3391
+ title?: string;
3392
+ subTitle?: RichText;
3393
+ helperText?: RichText;
3394
+ buttons: null;
3395
+ }
3396
+ > | null;
3397
+ } | null;
3398
+
3399
+ // Source: ../../packages/sanity/src/queries.ts
3400
+ // Variable: PAGE_QUERY
3401
+ // 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, "#" ) } } }}
3402
+ export type PAGE_QUERY_RESULT = {
3403
+ _id: string;
3404
+ _type: "page";
3405
+ title: string;
3406
+ description: string | null;
3407
+ slug: string;
3408
+ pageBuilder: Array<
3409
+ | {
3410
+ _key: string;
3411
+ _type: "cta";
3412
+ eyebrow?: string;
3413
+ title?: string;
3414
+ richText?: RichText;
3415
+ buttons: Array<{
3416
+ _key: string;
3417
+ _type: "button";
3418
+ text: string | null;
3419
+ variant: "default" | "link" | "outline" | "secondary" | null;
3420
+ openInNewTab: boolean | null;
3421
+ href: string | "/" | "#" | null;
3422
+ }> | null;
3423
+ }
3424
+ | {
3425
+ _key: string;
3426
+ _type: "faqAccordion";
3427
+ eyebrow?: string;
3428
+ title: string;
3429
+ subtitle?: string;
3430
+ faqs: Array<{
3431
+ _id: string;
3432
+ title: string;
3433
+ richText: RichText | null;
3434
+ }> | null;
3435
+ buttons: null;
3436
+ }
3437
+ | {
3438
+ _key: string;
3439
+ _type: "featureCardsIcon";
3440
+ eyebrow?: string;
3441
+ title?: string;
3442
+ richText?: RichText;
3443
+ cards?: Array<{
3444
+ icon?: string;
3445
+ title?: string;
3446
+ richText?: RichText;
3447
+ _type: "featureCardIcon";
3448
+ _key: string;
3449
+ }>;
3450
+ buttons: null;
3451
+ }
3452
+ | {
3453
+ _key: string;
3454
+ _type: "hero";
3455
+ badge?: string;
3456
+ title?: string;
3457
+ richText?: RichText;
3458
+ image?: Image1;
3459
+ buttons: Array<{
3460
+ _key: string;
3461
+ _type: "button";
3462
+ text: string | null;
3463
+ variant: "default" | "link" | "outline" | "secondary" | null;
3464
+ openInNewTab: boolean | null;
3465
+ href: string | "/" | "#" | null;
3466
+ }> | null;
3467
+ }
3468
+ | {
3469
+ _key: string;
3470
+ _type: "imageLinkCards";
3471
+ eyebrow?: string;
3472
+ title: string;
3473
+ richText?: RichText;
3474
+ buttons: Array<{
3475
+ _key: string;
3476
+ _type: "button";
3477
+ text: string | null;
3478
+ variant: "default" | "link" | "outline" | "secondary" | null;
3479
+ openInNewTab: boolean | null;
3480
+ href: string | "/" | "#" | null;
3481
+ }> | null;
3482
+ cards: Array<{
3483
+ title: string;
3484
+ description?: string;
3485
+ image?: Image1;
3486
+ url?: CustomUrl;
3487
+ _type: "imageLinkCard";
3488
+ _key: string;
3489
+ openInNewTab: boolean | null;
3490
+ href: string | "/" | "#" | null;
3491
+ }> | null;
3492
+ }
3493
+ | {
3494
+ _key: string;
3495
+ _type: "richTextBlock";
3496
+ eyebrow?: string;
3497
+ title?: string;
3498
+ richText?: RichText;
3499
+ buttons: null;
3500
+ }
3501
+ | {
3502
+ _key: string;
3503
+ _type: "subscribeNewsletter";
3504
+ title?: string;
3505
+ subTitle?: RichText;
3506
+ helperText?: RichText;
3507
+ buttons: null;
3508
+ }
3509
+ > | null;
1451
3510
  } | null;
1452
- `, ASTRO_PAGE_BUILDER = `---
1453
- import { PortableText } from "astro-portabletext";
1454
- import type { PageBlock } from "../../sanity/types";
1455
- import { urlFor } from "../../sanity/image";
1456
3511
 
1457
- interface Props {
1458
- blocks: PageBlock[];
1459
- }
3512
+ // Source: ../../packages/sanity/src/queries.ts
3513
+ // Variable: PAGE_SLUGS_QUERY
3514
+ // Query: *[_type == "page" && defined(slug.current) && slug.current != "home"].slug.current
3515
+ export type PAGE_SLUGS_QUERY_RESULT = Array<string>;
1460
3516
 
1461
- const { blocks } = Astro.props;
1462
- ---
3517
+ // Source: ../../packages/sanity/src/queries.ts
3518
+ // Variable: BLOG_INDEX_QUERY
3519
+ // Query: *[_type == "blog" && defined(slug.current)]|order(publishedAt desc){ _id, title, description, publishedAt, image, "slug": slug.current, "authors": authors[]->{ _id, name, position, image }}
3520
+ export type BLOG_INDEX_QUERY_RESULT = Array<{
3521
+ _id: string;
3522
+ title: string;
3523
+ description: string | null;
3524
+ publishedAt: string | null;
3525
+ image: {
3526
+ asset?: SanityImageAssetReference;
3527
+ media?: unknown;
3528
+ hotspot?: SanityImageHotspot;
3529
+ crop?: SanityImageCrop;
3530
+ alt?: string;
3531
+ _type: "image";
3532
+ } | null;
3533
+ slug: string;
3534
+ authors: Array<{
3535
+ _id: string;
3536
+ name: string;
3537
+ position: string | null;
3538
+ image: {
3539
+ asset?: SanityImageAssetReference;
3540
+ media?: unknown;
3541
+ hotspot?: SanityImageHotspot;
3542
+ crop?: SanityImageCrop;
3543
+ _type: "image";
3544
+ } | null;
3545
+ }> | null;
3546
+ }>;
3547
+
3548
+ // Source: ../../packages/sanity/src/queries.ts
3549
+ // Variable: BLOG_QUERY
3550
+ // Query: *[_type == "blog" && slug.current == $slug][0]{ _id, title, description, publishedAt, image, "slug": slug.current, "authors": authors[]->{ _id, name, position, image }, richText}
3551
+ export type BLOG_QUERY_RESULT = {
3552
+ _id: string;
3553
+ title: string;
3554
+ description: string | null;
3555
+ publishedAt: string | null;
3556
+ image: {
3557
+ asset?: SanityImageAssetReference;
3558
+ media?: unknown;
3559
+ hotspot?: SanityImageHotspot;
3560
+ crop?: SanityImageCrop;
3561
+ alt?: string;
3562
+ _type: "image";
3563
+ } | null;
3564
+ slug: string;
3565
+ authors: Array<{
3566
+ _id: string;
3567
+ name: string;
3568
+ position: string | null;
3569
+ image: {
3570
+ asset?: SanityImageAssetReference;
3571
+ media?: unknown;
3572
+ hotspot?: SanityImageHotspot;
3573
+ crop?: SanityImageCrop;
3574
+ _type: "image";
3575
+ } | null;
3576
+ }> | null;
3577
+ richText: RichText | null;
3578
+ } | null;
1463
3579
 
1464
- {
1465
- blocks.map((block) => {
1466
- if (block._type === "hero") {
1467
- const src = urlFor(block.image, 1600);
1468
- return (
1469
- <section>
1470
- <h2>{block.heading}</h2>
1471
- {block.subheading && <p>{block.subheading}</p>}
1472
- {src && <img src={src} alt={block.image?.alt ?? ""} />}
1473
- </section>
1474
- );
1475
- }
1476
- if (block._type === "richText") {
1477
- return (
1478
- <section>
1479
- {block.heading && <h2>{block.heading}</h2>}
1480
- {block.content && <PortableText value={block.content} />}
1481
- </section>
1482
- );
1483
- }
1484
- if (block._type === "cta") {
1485
- return (
1486
- <section>
1487
- <h2>{block.heading}</h2>
1488
- {block.text && <p>{block.text}</p>}
1489
- {block.buttonHref && block.buttonLabel && (
1490
- <a href={block.buttonHref}>{block.buttonLabel}</a>
1491
- )}
1492
- </section>
1493
- );
1494
- }
1495
- return null;
1496
- })
1497
- }
1498
- `, ASTRO_SLUG_PAGE = `---
1499
- import { VisualEditing } from "@sanity/astro/visual-editing";
1500
- import PageBuilder from "../components/PageBuilder.astro";
1501
- import { visualEditingEnabled } from "../../sanity/env";
1502
- import { loadQuery } from "../../sanity/fetch";
1503
- import { PAGE_QUERY, PAGE_SLUGS_QUERY } from "../../sanity/queries";
1504
- import type { SanityPage } from "../../sanity/types";
3580
+ // Source: ../../packages/sanity/src/queries.ts
3581
+ // Variable: BLOG_SLUGS_QUERY
3582
+ // Query: *[_type == "blog" && defined(slug.current)].slug.current
3583
+ export type BLOG_SLUGS_QUERY_RESULT = Array<string>;
3584
+
3585
+ // Source: ../../packages/sanity/src/queries.ts
3586
+ // Variable: SETTINGS_QUERY
3587
+ // Query: *[_type == "settings"][0]{ siteTitle, siteDescription, socialImage }
3588
+ export type SETTINGS_QUERY_RESULT = {
3589
+ siteTitle: string;
3590
+ siteDescription: string | null;
3591
+ socialImage: {
3592
+ asset?: SanityImageAssetReference;
3593
+ media?: unknown;
3594
+ hotspot?: SanityImageHotspot;
3595
+ crop?: SanityImageCrop;
3596
+ _type: "image";
3597
+ } | null;
3598
+ } | null;
1505
3599
 
1506
- export async function getStaticPaths() {
1507
- // Returns [] without credentials, so a fresh scaffold builds with no CMS pages.
1508
- const slugs = await loadQuery<(string | null)[]>(PAGE_SLUGS_QUERY);
1509
- return (slugs ?? []).filter(Boolean).map((slug) => ({ params: { slug } }));
3600
+ // Query TypeMap
3601
+ import "@sanity/client";
3602
+ declare module "@sanity/client" {
3603
+ interface SanityQueries {
3604
+ '*[_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;
3605
+ '*[_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;
3606
+ '*[_type == "page" && defined(slug.current) && slug.current != "home"].slug.current': PAGE_SLUGS_QUERY_RESULT;
3607
+ '*[_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;
3608
+ '*[_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;
3609
+ '*[_type == "blog" && defined(slug.current)].slug.current': BLOG_SLUGS_QUERY_RESULT;
3610
+ '*[_type == "settings"][0]{ siteTitle, siteDescription, socialImage }': SETTINGS_QUERY_RESULT;
3611
+ }
1510
3612
  }
3613
+ `;
1511
3614
 
1512
- const page = await loadQuery<SanityPage>(PAGE_QUERY, { slug: Astro.params.slug });
1513
- ---
1514
-
1515
- <html lang="en">
1516
- <head>
1517
- <meta charset="utf-8" />
1518
- <meta name="viewport" content="width=device-width, initial-scale=1" />
1519
- <title>{page?.title ?? "web"}</title>
1520
- </head>
1521
- <body style="font-family: system-ui, sans-serif; padding: 3rem;">
1522
- <h1>{page?.title}</h1>
1523
- <PageBuilder blocks={page?.pageBuilder ?? []} />
1524
- <VisualEditing enabled={visualEditingEnabled} />
1525
- </body>
1526
- </html>
1527
- `, ASTRO_SANITY_CONFIG, ASTRO_SANITY_CLI = `import { defineCliConfig } from "sanity/cli";
1528
-
1529
- // Run from apps/web. The Sanity CLI is a Node process, so it reads process.env rather
1530
- // than Astro's import.meta.env; astro.config.mjs loads apps/web/.env into it.
1531
- export default defineCliConfig({
1532
- api: {
1533
- projectId: process.env.PUBLIC_SANITY_PROJECT_ID || "placeholder",
1534
- dataset: process.env.PUBLIC_SANITY_DATASET || "production",
1535
- },
1536
- typegen: {
1537
- path: "./sanity/**/*.{ts,tsx}",
1538
- schema: "sanity/schema.json",
1539
- generates: "sanity/sanity.types.ts",
1540
- },
1541
- });
1542
- `, ASTRO_MODULE_REF = '/// <reference types="@sanity/astro/module" />', ASTRO_ENV_LOADER = `// Astro loads .env after this config runs, so pull it in ourselves for the Sanity ids.
1543
- try {
1544
- process.loadEnvFile(".env");
1545
- } catch {
1546
- // No .env — the placeholder ids below keep the build working.
1547
- }
1548
- `, prependAstroConfigStatement = (content, block, marker) => content.includes(marker) ? content : content.replace("export default defineConfig({", `${block}
3615
+ // ../../packages/registry/src/content/sanity/index.ts
3616
+ var j6 = (value) => `${JSON.stringify(value, null, 2)}
3617
+ `, SANITY_PACKAGE_JSON, SANITY_BLOCKS_PACKAGE_JSON, prependAstroConfigStatement = (content, block, marker) => content.includes(marker) ? content : content.replace("export default defineConfig({", `${block}
1549
3618
  export default defineConfig({`), addAstroOptimizeDeps = (content) => {
1550
3619
  if (content.includes("react/compiler-runtime"))
1551
3620
  return content;
@@ -1562,23 +3631,12 @@ export default defineConfig({`), addAstroOptimizeDeps = (content) => {
1562
3631
  return content.replace(/vite:\s*\{/, (m) => `${m} ${field}`);
1563
3632
  return content.replace(/defineConfig\(\{/, (m) => `${m}
1564
3633
  vite: { ${field} },`);
1565
- }, addScripts = (pkgJson, scripts) => {
1566
- const pkg = JSON.parse(pkgJson);
1567
- const merged = { ...pkg.scripts, ...scripts };
1568
- const sorted = Object.fromEntries(Object.entries(merged).sort(([a], [b]) => a.localeCompare(b)));
1569
- return `${JSON.stringify({ ...pkg, scripts: sorted }, null, 2)}
1570
- `;
1571
- }, TYPEGEN = "sanity schema extract --enforce-required-fields --path sanity/schema.json && sanity typegen generate", patchWebPackageJson = (content) => addScripts(content, {
1572
- "sanity:typegen": TYPEGEN,
1573
- prebuild: TYPEGEN,
1574
- ...content.includes('"next build"') ? { "precheck-types": TYPEGEN } : {},
1575
- "sanity:seed": "sanity dataset import sanity/seed.ndjson"
1576
- }), NEXT_LAYOUT_SIGNATURE = "export default function RootLayout({ children }: { children: ReactNode }) {", patchNextLayout = (content) => {
3634
+ }, NEXT_LAYOUT_SIGNATURE = "export default function RootLayout({ children }: { children: ReactNode }) {", patchNextLayout = (content) => {
1577
3635
  const withImports = [
1578
3636
  'import { draftMode } from "next/headers";',
1579
3637
  'import { VisualEditing } from "next-sanity/visual-editing";',
1580
3638
  'import { DisableDraftMode } from "../sanity/disable-draft-mode";',
1581
- 'import { SanityLive } from "../sanity/live";'
3639
+ 'import { SanityLive } from "@workspace/sanity/live";'
1582
3640
  ].reduce(ensureImport, content);
1583
3641
  const asAsync = replaceOnce(withImports, NEXT_LAYOUT_SIGNATURE, `export default async function RootLayout({ children }: { children: ReactNode }) {
1584
3642
  const { isEnabled: isDraft } = await draftMode();`);
@@ -1600,15 +3658,15 @@ export default defineConfig({`), addAstroOptimizeDeps = (content) => {
1600
3658
  return content;
1601
3659
  const withImports = [
1602
3660
  'import { VisualEditing } from "@sanity/astro/visual-editing";',
1603
- 'import PageBuilder from "../components/PageBuilder.astro";',
1604
- 'import { visualEditingEnabled } from "../../sanity/env";',
1605
- 'import { loadQuery } from "../../sanity/fetch";',
1606
- 'import { HOME_PAGE_QUERY } from "../../sanity/queries";',
1607
- 'import type { SanityPage } from "../../sanity/types";'
3661
+ 'import { visualEditingEnabled } from "@workspace/sanity/env";',
3662
+ 'import { loadQuery } from "@workspace/sanity/fetch";',
3663
+ 'import { HOME_PAGE_QUERY } from "@workspace/sanity/queries";',
3664
+ 'import type { HOME_PAGE_QUERY_RESULT } from "@workspace/sanity/types";',
3665
+ 'import PageBuilder from "@workspace/sanity-blocks/PageBuilder.astro";'
1608
3666
  ].reduce(ensureImport, content);
1609
3667
  const withFetch = replaceOnce(withImports, 'log.info("rendering home");', `log.info("rendering home");
1610
3668
 
1611
- const page = await loadQuery<SanityPage>(HOME_PAGE_QUERY);`);
3669
+ const page = await loadQuery<HOME_PAGE_QUERY_RESULT>(HOME_PAGE_QUERY);`);
1612
3670
  const withHeading = replaceOnce(withFetch, " <h1>create-turbo-wizard</h1>", ' <h1>{page?.title ?? "create-turbo-wizard"}</h1>');
1613
3671
  const withBlocks = replaceOnce(withHeading, " <p>Astro + Turborepo, scaffolded.</p>", [
1614
3672
  " <PageBuilder blocks={page?.pageBuilder ?? []} />",
@@ -1622,136 +3680,142 @@ const page = await loadQuery<SanityPage>(HOME_PAGE_QUERY);`);
1622
3680
  }, README_SECTION = `
1623
3681
  ## Sanity
1624
3682
 
1625
- The Studio is embedded in \`apps/web\` Next serves it at \`/studio\`, Astro at \`/admin\`.
1626
- Schema, queries and sample content live in \`apps/web/sanity/\`.
3683
+ The Studio is its own app, \`apps/studio\` (\`pnpm dev:studio\`, port 3333) a Vite app, so
3684
+ both frontends share the same one. It owns the document schemas, Studio structure and
3685
+ sample content. The read path (client, queries, generated types) is \`packages/sanity\` and
3686
+ the page-builder blocks — schemas and renderers together — are \`packages/sanity-blocks\`.
1627
3687
 
1628
3688
  \`\`\`sh
1629
- cd apps/web
1630
- cp ../../.env.example .env.local # Astro: .env — then fill in the project id
1631
- pnpm sanity:seed # import sanity/seed.ndjson into the configured dataset
1632
- pnpm sanity:typegen # regenerate sanity/sanity.types.ts (also runs on build)
3689
+ cp .env.example apps/web/.env.local # or apps/web/.env on Astro — the project id
3690
+ cp apps/studio/.env.example apps/studio/.env # and SANITY_STUDIO_PROJECT_ID
3691
+ pnpm dev:studio # the Studio on http://localhost:3333
3692
+ cd apps/studio && pnpm sanity:seed # import seed.ndjson into the dataset
3693
+ cd apps/studio && pnpm extract && pnpm type # regenerate packages/sanity/src/sanity.types.ts
1633
3694
  \`\`\`
1634
3695
 
3696
+ \`packages/sanity/src/sanity.types.ts\` ships pre-generated, so a fresh clone type-checks
3697
+ before anything is run; re-run \`extract\` + \`type\` after every schema or query edit.
3698
+
1635
3699
  Without a project id the site builds and serves an empty state — no credentials required.
1636
3700
 
3701
+ ### What's in the schema
3702
+
3703
+ \`page\` documents compose a page builder out of seven blocks — hero, rich text, feature
3704
+ cards, image link cards, FAQ accordion, newsletter and CTA — and \`blog\` posts carry an
3705
+ \`author\` reference, a publish date and a Portable Text body. Pages route by slug
3706
+ (\`/about\`), posts live under \`/blog\`. The shape follows Roboto Studio's
3707
+ \`turbo-start-sanity\`, so components and GROQ from that repo drop in unchanged.
3708
+
3709
+ Links are stored as \`customUrl\` (internal document or external URL) and flattened to a
3710
+ plain \`href\` in \`packages/sanity/src/queries.ts\`, so components never branch on the
3711
+ link type.
3712
+
1637
3713
  ### Visual Editing
1638
3714
 
1639
3715
  Next: open the Studio's **Presentation** tab — draft mode enables via
1640
3716
  \`/api/draft-mode/enable\`; leave it with the on-page "Disable draft mode" button.
1641
3717
  Astro: set \`PUBLIC_SANITY_VISUAL_EDITING_ENABLED=true\` and \`SANITY_API_READ_TOKEN\`,
1642
3718
  then use the dev server — overlays need server rendering (add an SSR adapter for a
1643
- production preview deployment).
3719
+ production preview deployment). \`PUBLIC_SANITY_STUDIO_URL\` is where stega sends a
3720
+ click-through; it defaults to the Studio's own dev origin.
1644
3721
  `, sanity;
1645
3722
  var init_sanity = __esm(() => {
1646
- NEXT_SANITY_CONFIG = `"use client";
1647
-
1648
- import { defineConfig } from "sanity";
1649
- import { presentationTool } from "sanity/presentation";
1650
- import { structureTool } from "sanity/structure";
1651
- import { locations, mainDocuments } from "./sanity/presentation";
1652
- import { schemaTypes } from "./sanity/schema/index";
1653
- import { singletonActions, singletonTypes, structure } from "./sanity/structure";
1654
-
1655
- export default defineConfig({
1656
- name: "default",
1657
- title: "Studio",
1658
- basePath: "/studio",
1659
- projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID || "placeholder",
1660
- dataset: process.env.NEXT_PUBLIC_SANITY_DATASET || "production",
1661
- plugins: [
1662
- presentationTool({
1663
- resolve: { mainDocuments, locations },
1664
- previewUrl: {
1665
- initial: "/",
1666
- previewMode: {
1667
- enable: "/api/draft-mode/enable",
1668
- // Presentation never calls this itself (documented as not yet implemented);
1669
- // editors leave draft mode via <DisableDraftMode/>, which links to the route.
1670
- disable: "/api/draft-mode/disable",
1671
- },
1672
- },
1673
- }),
1674
- structureTool({ structure }),
1675
- ],
1676
- schema: { types: schemaTypes },
1677
- ${DOCUMENT_GUARD}
1678
- });
1679
- `;
1680
- ASTRO_SANITY_CONFIG = `import { defineConfig } from "sanity";
1681
- import { presentationTool } from "sanity/presentation";
1682
- import { structureTool } from "sanity/structure";
1683
- import { locations, mainDocuments } from "./sanity/presentation";
1684
- import { schemaTypes } from "./sanity/schema/index";
1685
- import { singletonActions, singletonTypes, structure } from "./sanity/structure";
1686
-
1687
- export default defineConfig({
1688
- name: "default",
1689
- title: "Studio",
1690
- projectId: import.meta.env.PUBLIC_SANITY_PROJECT_ID || "placeholder",
1691
- dataset: import.meta.env.PUBLIC_SANITY_DATASET || "production",
1692
- plugins: [
1693
- presentationTool({
1694
- resolve: { mainDocuments, locations },
1695
- // No previewMode: Astro has no draft-mode routes. Draft content comes from
1696
- // loadQuery's perspective switch (PUBLIC_SANITY_VISUAL_EDITING_ENABLED + token),
1697
- // which needs a rendering server — \`astro dev\`, or an SSR adapter in production.
1698
- previewUrl: { initial: "/" },
1699
- }),
1700
- structureTool({ structure }),
1701
- ],
1702
- schema: { types: schemaTypes },
1703
- ${DOCUMENT_GUARD}
1704
- });
1705
- `;
3723
+ init_seed();
3724
+ init_studio();
3725
+ SANITY_PACKAGE_JSON = j6({
3726
+ name: "@workspace/sanity",
3727
+ version: "0.0.0",
3728
+ private: true,
3729
+ type: "module",
3730
+ exports: {
3731
+ "./env": "./src/env.ts",
3732
+ "./client": "./src/client.ts",
3733
+ "./live": "./src/live.ts",
3734
+ "./fetch": "./src/fetch.ts",
3735
+ "./image": "./src/image.ts",
3736
+ "./queries": "./src/queries.ts",
3737
+ "./types": "./src/sanity.types.ts"
3738
+ }
3739
+ });
3740
+ SANITY_BLOCKS_PACKAGE_JSON = j6({
3741
+ name: "@workspace/sanity-blocks",
3742
+ version: "0.0.0",
3743
+ private: true,
3744
+ type: "module",
3745
+ exports: {
3746
+ ".": "./src/schema.ts",
3747
+ "./blocks": "./src/blocks.tsx",
3748
+ "./PageBuilder.astro": "./src/PageBuilder.astro"
3749
+ }
3750
+ });
1706
3751
  sanity = {
1707
3752
  id: "sanity",
1708
3753
  kind: "content",
1709
3754
  prompt: {
1710
3755
  label: "Sanity",
1711
- hint: "Headless CMS with an embedded Studio (Next: /studio, Astro: /admin)"
3756
+ hint: "Headless CMS with a Studio of its own (apps/studio)"
1712
3757
  },
1713
3758
  templates: [
1714
- { path: "apps/web/sanity/schema/blocks.ts", content: SCHEMA_BLOCKS },
1715
- { path: "apps/web/sanity/schema/page.ts", content: SCHEMA_PAGE },
1716
- { path: "apps/web/sanity/schema/settings.ts", content: SCHEMA_SETTINGS },
1717
- { path: "apps/web/sanity/schema/index.ts", content: SCHEMA_INDEX },
1718
- { path: "apps/web/sanity/structure.ts", content: STRUCTURE },
1719
- { path: "apps/web/sanity/presentation.ts", content: PRESENTATION },
1720
- { path: "apps/web/sanity/queries.ts", content: QUERIES },
1721
- { path: "apps/web/sanity/seed.ndjson", content: SEED_NDJSON }
3759
+ { path: "packages/sanity/package.json", content: SANITY_PACKAGE_JSON },
3760
+ { path: "packages/sanity/src/queries.ts", content: QUERIES },
3761
+ { path: "packages/sanity/src/sanity.types.ts", content: SANITY_TYPES },
3762
+ { path: "packages/sanity-blocks/package.json", content: SANITY_BLOCKS_PACKAGE_JSON },
3763
+ { path: "packages/sanity-blocks/src/schema.ts", content: SCHEMA_BLOCKS },
3764
+ { path: "apps/studio/package.json", content: STUDIO_PACKAGE_JSON },
3765
+ { path: "apps/studio/tsconfig.json", content: STUDIO_TSCONFIG },
3766
+ { path: "apps/studio/sanity.cli.ts", content: STUDIO_CLI },
3767
+ { path: "apps/studio/schemaTypes/index.ts", content: SCHEMA_INDEX },
3768
+ { path: "apps/studio/schemaTypes/definitions.ts", content: SCHEMA_DEFINITIONS },
3769
+ { path: "apps/studio/schemaTypes/page.ts", content: SCHEMA_PAGE },
3770
+ { path: "apps/studio/schemaTypes/blog.ts", content: SCHEMA_BLOG },
3771
+ { path: "apps/studio/schemaTypes/settings.ts", content: SCHEMA_SETTINGS },
3772
+ { path: "apps/studio/structure.ts", content: STRUCTURE },
3773
+ { path: "apps/studio/presentation.ts", content: PRESENTATION },
3774
+ { path: "apps/studio/seed.ndjson", content: SEED_NDJSON }
1722
3775
  ],
1723
3776
  fragments: {
1724
3777
  next: [
1725
- { path: "apps/web/sanity/env.ts", content: NEXT_ENV },
1726
- { path: "apps/web/sanity/client.ts", content: NEXT_CLIENT },
1727
- { path: "apps/web/sanity/live.ts", content: NEXT_LIVE },
1728
- { path: "apps/web/sanity/fetch.ts", content: NEXT_FETCH },
1729
- { path: "apps/web/sanity/image.ts", content: NEXT_IMAGE },
1730
- { path: "apps/web/sanity/page-builder.tsx", content: NEXT_PAGE_BUILDER },
3778
+ { path: "packages/sanity/src/env.ts", content: NEXT_ENV },
3779
+ { path: "packages/sanity/src/client.ts", content: NEXT_CLIENT },
3780
+ { path: "packages/sanity/src/live.ts", content: NEXT_LIVE },
3781
+ { path: "packages/sanity/src/fetch.ts", content: NEXT_FETCH },
3782
+ { path: "packages/sanity/src/image.ts", content: NEXT_IMAGE },
3783
+ { path: "packages/sanity-blocks/src/blocks.tsx", content: NEXT_PAGE_BUILDER },
3784
+ { path: "apps/studio/sanity.config.ts", content: STUDIO_CONFIG_NEXT },
3785
+ { path: "apps/studio/.env.example", content: STUDIO_ENV_EXAMPLE_NEXT },
1731
3786
  { path: "apps/web/sanity/disable-draft-mode.tsx", content: NEXT_DISABLE_DRAFT_MODE },
1732
- { path: "apps/web/sanity.config.ts", content: NEXT_SANITY_CONFIG },
1733
- { path: "apps/web/sanity.cli.ts", content: NEXT_SANITY_CLI },
1734
- { path: "apps/web/app/studio/[[...tool]]/page.tsx", content: NEXT_STUDIO_PAGE },
1735
3787
  { path: "apps/web/app/api/draft-mode/enable/route.ts", content: NEXT_DRAFT_MODE_ROUTE },
1736
3788
  {
1737
3789
  path: "apps/web/app/api/draft-mode/disable/route.ts",
1738
3790
  content: NEXT_DRAFT_MODE_DISABLE_ROUTE
1739
3791
  },
1740
- { path: "apps/web/app/[slug]/page.tsx", content: NEXT_SLUG_PAGE }
3792
+ { path: "apps/web/app/[slug]/page.tsx", content: NEXT_SLUG_PAGE },
3793
+ { path: "apps/web/app/blog/page.tsx", content: NEXT_BLOG_INDEX_PAGE },
3794
+ { path: "apps/web/app/blog/[slug]/page.tsx", content: NEXT_BLOG_POST_PAGE }
1741
3795
  ],
1742
3796
  astro: [
1743
- { path: "apps/web/sanity/env.ts", content: ASTRO_ENV },
1744
- { path: "apps/web/sanity/fetch.ts", content: ASTRO_FETCH },
1745
- { path: "apps/web/sanity/image.ts", content: ASTRO_IMAGE },
1746
- { path: "apps/web/sanity/types.ts", content: ASTRO_TYPES },
1747
- { path: "apps/web/sanity.config.ts", content: ASTRO_SANITY_CONFIG },
1748
- { path: "apps/web/sanity.cli.ts", content: ASTRO_SANITY_CLI },
1749
- { path: "apps/web/src/components/PageBuilder.astro", content: ASTRO_PAGE_BUILDER },
1750
- { path: "apps/web/src/pages/[slug].astro", content: ASTRO_SLUG_PAGE }
3797
+ { path: "packages/sanity/src/env.ts", content: ASTRO_ENV },
3798
+ { path: "packages/sanity/src/fetch.ts", content: ASTRO_FETCH },
3799
+ { path: "packages/sanity/src/image.ts", content: ASTRO_IMAGE },
3800
+ { path: "packages/sanity-blocks/src/PageBuilder.astro", content: ASTRO_PAGE_BUILDER },
3801
+ { path: "apps/studio/sanity.config.ts", content: STUDIO_CONFIG_ASTRO },
3802
+ { path: "apps/studio/.env.example", content: STUDIO_ENV_EXAMPLE_ASTRO },
3803
+ { path: "apps/web/src/pages/[slug].astro", content: ASTRO_SLUG_PAGE },
3804
+ { path: "apps/web/src/pages/blog/index.astro", content: ASTRO_BLOG_INDEX_PAGE },
3805
+ { path: "apps/web/src/pages/blog/[slug].astro", content: ASTRO_BLOG_POST_PAGE }
1751
3806
  ]
1752
3807
  },
3808
+ scripts: {
3809
+ "dev:studio": "turbo run dev --filter=studio",
3810
+ "build:studio": "turbo run build --filter=studio"
3811
+ },
1753
3812
  deps: [
1754
- { name: "groq", version: "^6.7.0", target: "app" }
3813
+ { name: "@workspace/sanity", version: "workspace:*", target: "app" },
3814
+ { name: "@workspace/sanity-blocks", version: "workspace:*", target: "app" },
3815
+ { name: "groq", version: "^6.7.0", target: "sanity" },
3816
+ { name: "@sanity/client", version: "^7.26.0", target: "sanity" },
3817
+ { name: "sanity", version: "^5.31.1", target: "sanity-blocks" },
3818
+ { name: "@workspace/sanity", version: "workspace:*", target: "sanity-blocks" }
1755
3819
  ],
1756
3820
  frontendDeps: {
1757
3821
  next: [
@@ -1759,59 +3823,40 @@ ${DOCUMENT_GUARD}
1759
3823
  { name: "@portabletext/react", version: "^7.0.1", target: "app" },
1760
3824
  { name: "@sanity/image-url", version: "^2.1.1", target: "app" },
1761
3825
  { name: "@sanity/client", version: "^7.26.0", target: "app" },
1762
- { name: "sanity", version: "^5.31.1", target: "app" },
1763
- { name: "styled-components", version: "^6.1", target: "app" }
3826
+ { name: "next-sanity", version: "^13.2.3", target: "sanity" },
3827
+ { name: "@sanity/image-url", version: "^2.1.1", target: "sanity" },
3828
+ { name: "next", version: "^16.2.0", target: "sanity" },
3829
+ { name: "@portabletext/react", version: "^7.0.1", target: "sanity-blocks" },
3830
+ { name: "next", version: "^16.2.0", target: "sanity-blocks" },
3831
+ { name: "react", version: "^19.0.0", target: "sanity-blocks" }
1764
3832
  ],
1765
3833
  astro: [
1766
3834
  { name: "@sanity/astro", version: "^3.5.0", target: "app" },
1767
3835
  { name: "astro-portabletext", version: "^0.13.0", target: "app" },
1768
- { name: "@portabletext/types", version: "^4.0.2", target: "app" },
1769
3836
  { name: "@sanity/image-url", version: "^2.1.1", target: "app" },
1770
3837
  { name: "@sanity/client", version: "^7.26.0", target: "app" },
1771
- { name: "sanity", version: "^5.31.1", target: "app" },
1772
3838
  { name: "@sanity/ui", version: "^3.5.1", target: "app" },
1773
3839
  { name: "react-compiler-runtime", version: "^1.0.0", target: "app" },
1774
3840
  { name: "react-is", version: "^19.0.0", target: "app" },
1775
3841
  { name: "lodash", version: "^4.17.21", target: "app" },
1776
- { name: "styled-components", version: "^6.1", target: "app" },
1777
- { name: "@types/node", version: "^22.10.0", target: "app", dev: true }
3842
+ { name: "@sanity/image-url", version: "^2.1.1", target: "sanity" },
3843
+ { name: "astro-portabletext", version: "^0.13.0", target: "sanity-blocks" }
1778
3844
  ]
1779
3845
  },
1780
3846
  env: [
1781
3847
  { key: "NEXT_PUBLIC_SANITY_PROJECT_ID", scope: "client" },
1782
3848
  { key: "NEXT_PUBLIC_SANITY_DATASET", scope: "client" },
1783
3849
  { key: "NEXT_PUBLIC_SANITY_API_VERSION", scope: "client" },
3850
+ { key: "NEXT_PUBLIC_SANITY_STUDIO_URL", scope: "client" },
3851
+ { key: "SANITY_STUDIO_PROJECT_ID", scope: "server" },
3852
+ { key: "SANITY_STUDIO_DATASET", scope: "server" },
3853
+ { key: "SANITY_STUDIO_PRESENTATION_URL", scope: "server" },
1784
3854
  { key: "SANITY_API_READ_TOKEN", scope: "server" },
1785
3855
  { key: "PUBLIC_SANITY_PROJECT_ID", scope: "client" },
1786
3856
  { key: "PUBLIC_SANITY_DATASET", scope: "client" },
3857
+ { key: "PUBLIC_SANITY_STUDIO_URL", scope: "client" },
1787
3858
  { key: "PUBLIC_SANITY_VISUAL_EDITING_ENABLED", scope: "client" }
1788
3859
  ],
1789
- owns: [
1790
- "apps/web/sanity/schema/blocks.ts",
1791
- "apps/web/sanity/schema/page.ts",
1792
- "apps/web/sanity/schema/settings.ts",
1793
- "apps/web/sanity/schema/index.ts",
1794
- "apps/web/sanity/structure.ts",
1795
- "apps/web/sanity/presentation.ts",
1796
- "apps/web/sanity/queries.ts",
1797
- "apps/web/sanity/seed.ndjson",
1798
- "apps/web/sanity/env.ts",
1799
- "apps/web/sanity/client.ts",
1800
- "apps/web/sanity/live.ts",
1801
- "apps/web/sanity/fetch.ts",
1802
- "apps/web/sanity/image.ts",
1803
- "apps/web/sanity/types.ts",
1804
- "apps/web/sanity/page-builder.tsx",
1805
- "apps/web/sanity/disable-draft-mode.tsx",
1806
- "apps/web/sanity.config.ts",
1807
- "apps/web/sanity.cli.ts",
1808
- "apps/web/app/studio/[[...tool]]/page.tsx",
1809
- "apps/web/app/api/draft-mode/enable/route.ts",
1810
- "apps/web/app/api/draft-mode/disable/route.ts",
1811
- "apps/web/app/[slug]/page.tsx",
1812
- "apps/web/src/components/PageBuilder.astro",
1813
- "apps/web/src/pages/[slug].astro"
1814
- ],
1815
3860
  patches: [
1816
3861
  {
1817
3862
  path: "apps/web/next.config.ts",
@@ -1821,14 +3866,13 @@ ${DOCUMENT_GUARD}
1821
3866
  { path: "apps/web/app/page.tsx", apply: replaceHomePage(NEXT_HOME_PAGE) },
1822
3867
  {
1823
3868
  path: "apps/web/astro.config.mjs",
1824
- apply: (content) => addAstroOptimizeDeps(addAstroIntegration(prependAstroConfigStatement(content, ASTRO_ENV_LOADER, "process.loadEnvFile"), 'import sanity from "@sanity/astro";', `sanity({ projectId: process.env.PUBLIC_SANITY_PROJECT_ID ?? 'placeholder', dataset: process.env.PUBLIC_SANITY_DATASET ?? 'production', apiVersion: '2026-02-01', useCdn: false, studioBasePath: '/admin', stega: { studioUrl: '/admin' } })`))
3869
+ apply: (content) => addAstroOptimizeDeps(addAstroIntegration(prependAstroConfigStatement(content, ASTRO_ENV_LOADER, "process.loadEnvFile"), 'import sanity from "@sanity/astro";', `sanity({ projectId: process.env.PUBLIC_SANITY_PROJECT_ID ?? 'placeholder', dataset: process.env.PUBLIC_SANITY_DATASET ?? 'production', apiVersion: '2026-02-01', useCdn: false, stega: { studioUrl: process.env.PUBLIC_SANITY_STUDIO_URL ?? 'http://localhost:3333' } })`))
1825
3870
  },
1826
3871
  {
1827
3872
  path: "apps/web/src/env.d.ts",
1828
3873
  apply: (content) => ensureImport(content, ASTRO_MODULE_REF)
1829
3874
  },
1830
3875
  { path: "apps/web/src/pages/index.astro", apply: patchAstroIndex },
1831
- { path: "apps/web/package.json", apply: patchWebPackageJson },
1832
3876
  {
1833
3877
  path: "README.md",
1834
3878
  apply: (content) => content.includes("## Sanity") ? content : content + README_SECTION
@@ -1838,7 +3882,7 @@ ${DOCUMENT_GUARD}
1838
3882
  });
1839
3883
 
1840
3884
  // ../../packages/registry/src/frontends/astro.ts
1841
- var j4 = (value) => `${JSON.stringify(value, null, 2)}
3885
+ var j7 = (value) => `${JSON.stringify(value, null, 2)}
1842
3886
  `, ASTRO_CONFIG = `import react from "@astrojs/react";
1843
3887
  import { defineConfig } from "astro/config";
1844
3888
 
@@ -1868,10 +3912,14 @@ var init_astro = __esm(() => {
1868
3912
  astroFrontend = {
1869
3913
  id: "astro",
1870
3914
  kind: "frontend",
3915
+ prompt: {
3916
+ label: "Astro",
3917
+ hint: "Islands architecture, zero client JS by default"
3918
+ },
1871
3919
  templates: [
1872
3920
  {
1873
3921
  path: "apps/web/package.json",
1874
- content: j4({
3922
+ content: j7({
1875
3923
  name: "web",
1876
3924
  version: "0.0.0",
1877
3925
  private: true,
@@ -1903,7 +3951,7 @@ var init_astro = __esm(() => {
1903
3951
  { path: "apps/web/astro.config.mjs", content: ASTRO_CONFIG },
1904
3952
  {
1905
3953
  path: "apps/web/tsconfig.json",
1906
- content: j4({
3954
+ content: j7({
1907
3955
  extends: "astro/tsconfigs/strict",
1908
3956
  compilerOptions: { jsx: "react-jsx", jsxImportSource: "react" },
1909
3957
  include: [".astro/types.d.ts", "**/*"],
@@ -1913,17 +3961,44 @@ var init_astro = __esm(() => {
1913
3961
  { path: "apps/web/src/env.d.ts", content: `/// <reference types="astro/client" />
1914
3962
  ` },
1915
3963
  { path: "apps/web/src/pages/index.astro", content: INDEX_ASTRO }
1916
- ],
1917
- owns: [
1918
- "apps/web/package.json",
1919
- "apps/web/astro.config.mjs",
1920
- "apps/web/tsconfig.json",
1921
- "apps/web/src/env.d.ts",
1922
- "apps/web/src/pages/index.astro"
1923
3964
  ]
1924
3965
  };
1925
3966
  });
1926
3967
 
3968
+ // ../../packages/registry/src/frontends/deploy-target.ts
3969
+ 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) => {
3970
+ if (config.frontend !== "astro" || config.deploy !== "vercel")
3971
+ return null;
3972
+ if (!config.modules.some((id) => ASTRO_SSR_MODULES.includes(id)))
3973
+ return null;
3974
+ return {
3975
+ id: "deploy-vercel",
3976
+ kind: "frontend",
3977
+ frontendDeps: {
3978
+ astro: [{ name: "@astrojs/vercel", version: ASTROJS_VERCEL_VERSION, target: "app" }]
3979
+ },
3980
+ patches: [
3981
+ {
3982
+ path: "apps/web/astro.config.mjs",
3983
+ apply: (content) => {
3984
+ if (content.includes(VERCEL_IMPORT))
3985
+ return content;
3986
+ if (!content.includes(NODE_IMPORT))
3987
+ return content;
3988
+ return content.replace(NODE_IMPORT, VERCEL_IMPORT).replace(NODE_ADAPTER, VERCEL_ADAPTER);
3989
+ }
3990
+ },
3991
+ {
3992
+ path: "apps/web/package.json",
3993
+ apply: (content) => content.includes('"@astrojs/node"') ? content.replace(/^\s*"@astrojs\/node":\s*"[^"]*",?\n/m, "") : content
3994
+ }
3995
+ ]
3996
+ };
3997
+ };
3998
+ var init_deploy_target = __esm(() => {
3999
+ ASTRO_SSR_MODULES = ["forms"];
4000
+ });
4001
+
1927
4002
  // ../../packages/registry/src/frontends/experimental.ts
1928
4003
  var BABEL_REACT_COMPILER_VERSION = "^1.0.0", experimentalDescriptor = (config) => {
1929
4004
  const { cacheComponents, reactCompiler } = config.experimental;
@@ -1960,7 +4035,7 @@ var BABEL_REACT_COMPILER_VERSION = "^1.0.0", experimentalDescriptor = (config) =
1960
4035
  var init_experimental = () => {};
1961
4036
 
1962
4037
  // ../../packages/registry/src/frontends/next.ts
1963
- var j5 = (value) => `${JSON.stringify(value, null, 2)}
4038
+ var j8 = (value) => `${JSON.stringify(value, null, 2)}
1964
4039
  `, NEXT_CONFIG = `import { createRequire } from "node:module";
1965
4040
  import type { NextConfig } from "next";
1966
4041
 
@@ -2016,10 +4091,14 @@ var init_next = __esm(() => {
2016
4091
  nextFrontend = {
2017
4092
  id: "next",
2018
4093
  kind: "frontend",
4094
+ prompt: {
4095
+ label: "Next.js",
4096
+ hint: "App Router, React Server Components, Turbopack"
4097
+ },
2019
4098
  templates: [
2020
4099
  {
2021
4100
  path: "apps/web/package.json",
2022
- content: j5({
4101
+ content: j8({
2023
4102
  name: "web",
2024
4103
  version: "0.0.0",
2025
4104
  private: true,
@@ -2050,7 +4129,7 @@ var init_next = __esm(() => {
2050
4129
  { path: "apps/web/next.config.ts", content: NEXT_CONFIG },
2051
4130
  {
2052
4131
  path: "apps/web/tsconfig.json",
2053
- content: j5({
4132
+ content: j8({
2054
4133
  extends: "@workspace/typescript-config/nextjs.json",
2055
4134
  compilerOptions: { plugins: [{ name: "next" }], paths: { "@/*": ["./*"] } },
2056
4135
  include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
@@ -2060,14 +4139,6 @@ var init_next = __esm(() => {
2060
4139
  { path: "apps/web/app/layout.tsx", content: LAYOUT },
2061
4140
  { path: "apps/web/app/page.tsx", content: PAGE },
2062
4141
  { path: "apps/web/app/globals.css", content: GLOBALS }
2063
- ],
2064
- owns: [
2065
- "apps/web/package.json",
2066
- "apps/web/next.config.ts",
2067
- "apps/web/tsconfig.json",
2068
- "apps/web/app/layout.tsx",
2069
- "apps/web/app/page.tsx",
2070
- "apps/web/app/globals.css"
2071
4142
  ]
2072
4143
  };
2073
4144
  });
@@ -2236,18 +4307,12 @@ export default function AlgoliaSearch({
2236
4307
  { key: "PUBLIC_ALGOLIA_APP_ID", scope: "client" },
2237
4308
  { key: "PUBLIC_ALGOLIA_SEARCH_KEY", scope: "client" },
2238
4309
  { key: "PUBLIC_ALGOLIA_INDEX", scope: "client" }
2239
- ],
2240
- owns: [
2241
- "apps/web/app/search/Search.tsx",
2242
- "apps/web/app/search/page.tsx",
2243
- "apps/web/src/components/AlgoliaSearch.tsx",
2244
- "apps/web/src/pages/search.astro"
2245
4310
  ]
2246
4311
  };
2247
4312
  });
2248
4313
 
2249
4314
  // ../../packages/registry/src/modules/changesets.ts
2250
- var j6 = (v) => `${JSON.stringify(v, null, 2)}
4315
+ var j9 = (v) => `${JSON.stringify(v, null, 2)}
2251
4316
  `, CONFIG, README = `# Changesets
2252
4317
 
2253
4318
  This folder is managed by [Changesets](https://github.com/changesets/changesets).
@@ -2258,7 +4323,7 @@ the \`version-packages\` script consumes them to update versions and changelogs,
2258
4323
  the \`release\` script builds and publishes.
2259
4324
  `, changesets;
2260
4325
  var init_changesets = __esm(() => {
2261
- CONFIG = j6({
4326
+ CONFIG = j9({
2262
4327
  $schema: "https://unpkg.com/@changesets/config@3.1.4/schema.json",
2263
4328
  changelog: "@changesets/cli/changelog",
2264
4329
  commit: false,
@@ -2286,8 +4351,7 @@ var init_changesets = __esm(() => {
2286
4351
  changeset: "changeset",
2287
4352
  "version-packages": "changeset version",
2288
4353
  release: "turbo run build && changeset publish"
2289
- },
2290
- owns: [".changeset/config.json", ".changeset/README.md"]
4354
+ }
2291
4355
  };
2292
4356
  });
2293
4357
 
@@ -2336,8 +4400,7 @@ var init_e2e = __esm(() => {
2336
4400
  ]
2337
4401
  },
2338
4402
  scripts: { e2e: "playwright test" },
2339
- deps: [{ name: "@playwright/test", version: "^1.60.0", target: "root", dev: true }],
2340
- owns: ["tests/e2e/home.spec.ts", "playwright.config.ts"]
4403
+ deps: [{ name: "@playwright/test", version: "^1.60.0", target: "root", dev: true }]
2341
4404
  };
2342
4405
  });
2343
4406
 
@@ -2577,13 +4640,6 @@ export const POST: APIRoute = async ({ request }) => {
2577
4640
  path: "apps/web/astro.config.mjs",
2578
4641
  apply: (content) => ensureAstroAdapter(content, 'import node from "@astrojs/node";', 'node({ mode: "standalone" })')
2579
4642
  }
2580
- ],
2581
- owns: [
2582
- "apps/web/app/contact/page.tsx",
2583
- "apps/web/app/api/contact/route.ts",
2584
- "apps/web/src/components/ContactForm.tsx",
2585
- "apps/web/src/pages/contact.astro",
2586
- "apps/web/src/pages/api/contact.ts"
2587
4643
  ]
2588
4644
  };
2589
4645
  });
@@ -2625,8 +4681,7 @@ var init_git_hooks = __esm(() => {
2625
4681
  target: "root",
2626
4682
  dev: true
2627
4683
  }
2628
- ],
2629
- owns: ["lefthook.yml", "commitlint.config.mjs"]
4684
+ ]
2630
4685
  };
2631
4686
  });
2632
4687
 
@@ -2680,8 +4735,7 @@ var init_github_ci = __esm(() => {
2680
4735
  label: "GitHub Actions CI",
2681
4736
  hint: "Workflow that installs, type-checks, and builds on every push and PR."
2682
4737
  },
2683
- templates: [{ path: ".github/workflows/ci.yml", content: CI_WORKFLOW }],
2684
- owns: [".github/workflows/ci.yml"]
4738
+ templates: [{ path: ".github/workflows/ci.yml", content: CI_WORKFLOW }]
2685
4739
  };
2686
4740
  });
2687
4741
 
@@ -2844,18 +4898,7 @@ ${LLMS_TXT_REST}\`;
2844
4898
  { path: "apps/web/src/content/pages.ts", content: ASTRO_CONTENT },
2845
4899
  { path: "apps/web/src/pages/[slug].md.ts", content: ASTRO_MD_ENDPOINT }
2846
4900
  ]
2847
- },
2848
- owns: [
2849
- "skills.md",
2850
- "apps/web/app/llms.txt/route.ts",
2851
- "apps/web/app/docs/content.ts",
2852
- "apps/web/app/docs/page.tsx",
2853
- "apps/web/app/docs.md/route.ts",
2854
- "apps/web/app/lib/markdown-variant.ts",
2855
- "apps/web/src/pages/llms.txt.ts",
2856
- "apps/web/src/content/pages.ts",
2857
- "apps/web/src/pages/[slug].md.ts"
2858
- ]
4901
+ }
2859
4902
  };
2860
4903
  });
2861
4904
 
@@ -2916,7 +4959,6 @@ var init_posthog = __esm(() => {
2916
4959
  { key: "PUBLIC_POSTHOG_KEY", scope: "client" },
2917
4960
  { key: "PUBLIC_POSTHOG_HOST", scope: "client" }
2918
4961
  ],
2919
- owns: ["apps/web/instrumentation-client.ts", "apps/web/src/components/PostHog.astro"],
2920
4962
  patches: [
2921
4963
  { path: "apps/web/src/pages/index.astro", apply: patchIndexAstro },
2922
4964
  { path: "apps/web/src/layouts/Layout.astro", apply: patchIndexAstro }
@@ -3444,15 +5486,6 @@ var init_sections = __esm(() => {
3444
5486
  { path: "packages/ui/src/components/sections/faq.tsx", content: FAQ_TSX },
3445
5487
  { path: "packages/ui/src/components/sections/cta.tsx", content: CTA_TSX }
3446
5488
  ],
3447
- owns: [
3448
- "packages/ui/src/components/sections/hero.tsx",
3449
- "packages/ui/src/components/sections/features.tsx",
3450
- "packages/ui/src/components/sections/logo-cloud.tsx",
3451
- "packages/ui/src/components/sections/testimonials.tsx",
3452
- "packages/ui/src/components/sections/pricing.tsx",
3453
- "packages/ui/src/components/sections/faq.tsx",
3454
- "packages/ui/src/components/sections/cta.tsx"
3455
- ],
3456
5489
  patches: [
3457
5490
  { path: "apps/web/app/page.tsx", apply: replaceWith(COMPOSED_NEXT_HOME_PAGE) },
3458
5491
  { path: "apps/web/src/pages/index.astro", apply: replaceWith(COMPOSED_ASTRO_HOME_PAGE) }
@@ -3720,14 +5753,6 @@ var init_seo = __esm(() => {
3720
5753
  { key: "PUBLIC_SITE_NAME", scope: "client" },
3721
5754
  { key: "PUBLIC_SITE_DESCRIPTION", scope: "client" }
3722
5755
  ],
3723
- owns: [
3724
- "apps/web/app/lib/metadata.ts",
3725
- "apps/web/app/sitemap.ts",
3726
- "apps/web/app/robots.ts",
3727
- "apps/web/app/opengraph-image.tsx",
3728
- "apps/web/src/components/SEO.astro",
3729
- "apps/web/src/pages/og.png.ts"
3730
- ],
3731
5756
  patches: [
3732
5757
  { path: "apps/web/astro.config.mjs", apply: patchAstroConfig },
3733
5758
  { path: "apps/web/app/layout.tsx", apply: patchLayout },
@@ -4048,7 +6073,7 @@ export function SiteHeader({
4048
6073
  ))}
4049
6074
  </nav>
4050
6075
  <div className="ml-auto">
4051
- <Button size="sm" render={<a href={action.href} />}>
6076
+ <Button size="sm" nativeButton={false} render={<a href={action.href} />}>
4052
6077
  {action.label}
4053
6078
  </Button>
4054
6079
  </div>
@@ -4111,7 +6136,9 @@ export default function NotFound() {
4111
6136
  <p className="text-muted-foreground">
4112
6137
  The link may be out of date, or the page may have moved.
4113
6138
  </p>
4114
- <Button render={<a href="/" />}>Back home</Button>
6139
+ <Button nativeButton={false} render={<a href="/" />}>
6140
+ Back home
6141
+ </Button>
4115
6142
  </main>
4116
6143
  );
4117
6144
  }
@@ -4562,8 +6589,8 @@ ${DARK_TOKENS}
4562
6589
  });
4563
6590
 
4564
6591
  // ../../packages/registry/src/modules/ui/index.ts
4565
- var j7 = (value) => `${JSON.stringify(value, null, 2)}
4566
- `, UI_PACKAGE_JSON, componentsJson = (rsc) => j7({
6592
+ var j10 = (value) => `${JSON.stringify(value, null, 2)}
6593
+ `, UI_PACKAGE_JSON, componentsJson = (rsc) => j10({
4567
6594
  $schema: "https://ui.shadcn.com/schema.json",
4568
6595
  style: "base-nova",
4569
6596
  rsc,
@@ -4586,20 +6613,22 @@ var j7 = (value) => `${JSON.stringify(value, null, 2)}
4586
6613
  menuColor: "default",
4587
6614
  menuAccent: "subtle"
4588
6615
  }), patchNextLayout2 = (content) => {
4589
- if (content.includes("@workspace/ui/components/site-header"))
6616
+ if (content.includes("<SiteHeader"))
4590
6617
  return content;
4591
6618
  const withImports = [
4592
6619
  'import { SiteFooter } from "@workspace/ui/components/site-footer";',
4593
6620
  'import { SiteHeader } from "@workspace/ui/components/site-header";',
4594
- 'import rootPkg from "../../../package.json";',
4595
- content
6621
+ 'import rootPkg from "../../../package.json";'
6622
+ ].reduce(ensureImport, content);
6623
+ const expanded = withImports.replace(/^([ \t]*)<body([^>]*)>\{children\}<\/body>/m, (_m, indent, attrs) => `${indent}<body${attrs}>
6624
+ ${indent} {children}
6625
+ ${indent}</body>`);
6626
+ return expanded.replace(/<body([^>]*)>/, '<body$1 className="flex min-h-svh flex-col">').replace(/^([ \t]*)\{children\}$/m, (_m, indent) => [
6627
+ `${indent}<SiteHeader siteName={rootPkg.name} />`,
6628
+ `${indent}<div className="flex-1">{children}</div>`,
6629
+ `${indent}<SiteFooter siteName={rootPkg.name} />`
4596
6630
  ].join(`
4597
- `);
4598
- return withImports.replace(/<body([^>]*)>\{children\}<\/body>/, `<body$1 className="flex min-h-svh flex-col">
4599
- <SiteHeader siteName={rootPkg.name} />
4600
- <div className="flex-1">{children}</div>
4601
- <SiteFooter siteName={rootPkg.name} />
4602
- </body>`);
6631
+ `));
4603
6632
  }, NEXT_POSTCSS_CONFIG = `export default {
4604
6633
  plugins: {
4605
6634
  "@tailwindcss/postcss": {},
@@ -4610,7 +6639,7 @@ var j7 = (value) => `${JSON.stringify(value, null, 2)}
4610
6639
  var init_ui = __esm(() => {
4611
6640
  init_pages();
4612
6641
  init_theme();
4613
- UI_PACKAGE_JSON = j7({
6642
+ UI_PACKAGE_JSON = j10({
4614
6643
  name: "@workspace/ui",
4615
6644
  version: "0.0.0",
4616
6645
  private: true,
@@ -4674,25 +6703,6 @@ var init_ui = __esm(() => {
4674
6703
  ]
4675
6704
  },
4676
6705
  deps: [{ name: "@workspace/ui", version: "workspace:*", target: "app" }],
4677
- owns: [
4678
- "packages/ui/package.json",
4679
- "packages/ui/components.json",
4680
- "packages/ui/src/styles/globals.css",
4681
- "packages/ui/src/lib/utils.ts",
4682
- "packages/ui/src/components/button.tsx",
4683
- "packages/ui/src/components/card.tsx",
4684
- "packages/ui/src/components/input.tsx",
4685
- "packages/ui/src/components/textarea.tsx",
4686
- "packages/ui/src/components/label.tsx",
4687
- "packages/ui/src/components/badge.tsx",
4688
- "packages/ui/src/components/site-header.tsx",
4689
- "packages/ui/src/components/site-footer.tsx",
4690
- "apps/web/postcss.config.mjs",
4691
- "apps/web/components.json",
4692
- "apps/web/app/not-found.tsx",
4693
- "apps/web/src/layouts/Layout.astro",
4694
- "apps/web/src/pages/404.astro"
4695
- ],
4696
6706
  patches: [
4697
6707
  {
4698
6708
  path: "apps/web/app/globals.css",
@@ -4710,7 +6720,7 @@ var init_ui = __esm(() => {
4710
6720
  if (!plugin || pkg.devDependencies?.[plugin])
4711
6721
  return content;
4712
6722
  const devDependencies = Object.fromEntries(Object.entries({ ...pkg.devDependencies, [plugin]: "^4.3.0" }).sort(([a], [b]) => a.localeCompare(b)));
4713
- return j7({ ...pkg, devDependencies });
6723
+ return j10({ ...pkg, devDependencies });
4714
6724
  }
4715
6725
  },
4716
6726
  { path: "apps/web/app/layout.tsx", apply: patchNextLayout2 },
@@ -4751,21 +6761,51 @@ var init_vitest = __esm(() => {
4751
6761
  { path: "tests/example.test.ts", content: EXAMPLE_TEST }
4752
6762
  ],
4753
6763
  scripts: { test: "vitest run", "test:watch": "vitest" },
4754
- deps: [{ name: "vitest", version: "^4.1.0", target: "root", dev: true }],
4755
- owns: ["vitest.config.ts", "tests/example.test.ts"]
6764
+ deps: [{ name: "vitest", version: "^4.1.0", target: "root", dev: true }]
4756
6765
  };
4757
6766
  });
4758
6767
 
6768
+ // ../../packages/registry/src/catalog.ts
6769
+ function withPrerequisites(selected) {
6770
+ const chosen = new Set(selected);
6771
+ const added = [];
6772
+ const queue = [...selected];
6773
+ for (const id of queue) {
6774
+ for (const required of MODULE_DESCRIPTORS[id]?.compat?.requiresModules ?? []) {
6775
+ if (chosen.has(required))
6776
+ continue;
6777
+ chosen.add(required);
6778
+ added.push([required, id]);
6779
+ queue.push(required);
6780
+ }
6781
+ }
6782
+ return { modules: MODULES.filter((id) => chosen.has(id)), added };
6783
+ }
6784
+ var entry = (id, descriptor) => ({
6785
+ id,
6786
+ label: descriptor?.prompt?.label ?? id,
6787
+ hint: descriptor?.prompt?.hint ?? "",
6788
+ category: descriptor?.category,
6789
+ requires: descriptor?.compat?.requiresModules ?? []
6790
+ }), moduleCatalog = () => MODULES.map((id) => entry(id, MODULE_DESCRIPTORS[id])), frontendCatalog = () => FRONTENDS.map((id) => entry(id, FRONTEND_DESCRIPTORS[id])), NO_CONTENT, contentCatalog = () => CONTENT_SOURCES.map((id) => id === "none" ? { id, ...NO_CONTENT, category: undefined, requires: [] } : entry(id, CONTENT_DESCRIPTORS[id]));
6791
+ var init_catalog = __esm(() => {
6792
+ init_src();
6793
+ init_src3();
6794
+ NO_CONTENT = { label: "None", hint: "No CMS and no MDX — just the frontend" };
6795
+ });
6796
+
4759
6797
  // ../../packages/registry/src/index.ts
4760
6798
  function buildRegistry(config2) {
4761
6799
  const content = CONTENT_DESCRIPTORS[config2.content];
4762
6800
  const experimental = experimentalDescriptor(config2);
6801
+ const deploy = deployDescriptor(config2);
4763
6802
  return [
4764
6803
  ...baselineDescriptors,
4765
6804
  FRONTEND_DESCRIPTORS[config2.frontend],
4766
6805
  ...experimental ? [experimental] : [],
4767
6806
  ...content ? [content] : [],
4768
- ...config2.modules.map((id) => MODULE_DESCRIPTORS[id])
6807
+ ...config2.modules.map((id) => MODULE_DESCRIPTORS[id]),
6808
+ ...deploy ? [deploy] : []
4769
6809
  ];
4770
6810
  }
4771
6811
  var baselineDescriptors, FRONTEND_DESCRIPTORS, MODULE_DESCRIPTORS, CONTENT_DESCRIPTORS;
@@ -4777,6 +6817,7 @@ var init_src3 = __esm(() => {
4777
6817
  init_markdown();
4778
6818
  init_sanity();
4779
6819
  init_astro();
6820
+ init_deploy_target();
4780
6821
  init_experimental();
4781
6822
  init_next();
4782
6823
  init_algolia();
@@ -4791,6 +6832,7 @@ var init_src3 = __esm(() => {
4791
6832
  init_seo();
4792
6833
  init_ui();
4793
6834
  init_vitest();
6835
+ init_catalog();
4794
6836
  baselineDescriptors = [typescriptConfig, env, logger, biome];
4795
6837
  FRONTEND_DESCRIPTORS = {
4796
6838
  next: nextFrontend,
@@ -4817,40 +6859,82 @@ var init_src3 = __esm(() => {
4817
6859
  });
4818
6860
 
4819
6861
  // src/create.ts
4820
- import { spawnSync } from "node:child_process";
6862
+ import { spawn, spawnSync } from "node:child_process";
4821
6863
  import { mkdir as mkdir2 } from "node:fs/promises";
4822
6864
  import { resolve } from "node:path";
4823
- function step(label, cmd, args, cwd, stdio = "ignore") {
6865
+ function step(label, cmd, args, cwd, stdio = "ignore", onWarn = warn) {
4824
6866
  const res = spawnSync(cmd, args, { cwd, stdio });
4825
6867
  if (res.error) {
4826
- console.error(`! ${label} skipped: ${res.error.message}`);
6868
+ onWarn(`${label} skipped: ${res.error.message}`);
4827
6869
  return false;
4828
6870
  }
4829
6871
  if (res.status !== 0) {
4830
- console.error(`! ${label} failed (exit ${res.status ?? `signal ${res.signal}`})`);
6872
+ onWarn(`${label} failed (exit ${res.status ?? `signal ${res.signal}`})`);
4831
6873
  return false;
4832
6874
  }
4833
6875
  return true;
4834
6876
  }
4835
- async function createProject(config2, opts = {}) {
4836
- const res = generate(config2, buildRegistry(config2));
4837
- if (!res.ok)
4838
- return res;
6877
+ function streamStep(label, cmd, args, cwd, onOutput, onWarn) {
6878
+ return new Promise((done) => {
6879
+ const child = spawn(cmd, args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
6880
+ const emit = (chunk) => {
6881
+ for (const line of chunk.toString().split(/\r?\n|\r/)) {
6882
+ if (line.trim())
6883
+ onOutput(line);
6884
+ }
6885
+ };
6886
+ child.stdout?.on("data", emit);
6887
+ child.stderr?.on("data", emit);
6888
+ child.on("error", (error) => {
6889
+ onWarn(`${label} skipped: ${error.message}`);
6890
+ done(false);
6891
+ });
6892
+ child.on("close", (code, signal) => {
6893
+ if (code === 0)
6894
+ return done(true);
6895
+ onWarn(`${label} failed (exit ${code ?? `signal ${signal}`})`);
6896
+ done(false);
6897
+ });
6898
+ });
6899
+ }
6900
+ function planProject(config2) {
6901
+ return generate(config2, buildRegistry(config2));
6902
+ }
6903
+ async function writeProject(tree, config2, opts = {}) {
6904
+ const { onWarn = warn, onOutput } = opts;
4839
6905
  const dir = resolve(opts.targetDir ?? config2.name);
4840
6906
  await mkdir2(dir, { recursive: true });
4841
- await writeTree(res.value, dir);
6907
+ await writeTree(tree, dir);
4842
6908
  if (opts.git) {
4843
- const inited = step("git init", "git", ["init", "-q"], dir);
4844
- const added = inited && step("git add", "git", ["add", "-A"], dir);
6909
+ const inited = step("git init", "git", ["init", "-q"], dir, "ignore", onWarn);
6910
+ const added = inited && step("git add", "git", ["add", "-A"], dir, "ignore", onWarn);
4845
6911
  if (added) {
4846
- step("git commit", "git", ["commit", "-qm", "chore: scaffold with create-turbo-wizard"], dir);
6912
+ const args = ["commit", "-qm", "chore: scaffold with create-turbo-wizard"];
6913
+ step("git commit", "git", args, dir, "ignore", onWarn);
4847
6914
  }
4848
6915
  }
6916
+ let installError;
4849
6917
  if (opts.install) {
4850
- step(`${config2.packageManager} install`, config2.packageManager, ["install"], dir, "inherit");
6918
+ const label = `${config2.packageManager} install`;
6919
+ const capture = (message) => {
6920
+ installError = message;
6921
+ };
6922
+ const ok2 = onOutput ? await streamStep(label, config2.packageManager, ["install"], dir, onOutput, capture) : step(label, config2.packageManager, ["install"], dir, "inherit", capture);
6923
+ if (ok2)
6924
+ installError = null;
4851
6925
  }
4852
- return { ok: true, value: { dir, fileCount: res.value.size } };
6926
+ return { dir, fileCount: tree.size, installError };
6927
+ }
6928
+ async function createProject(config2, opts = {}) {
6929
+ const res = planProject(config2);
6930
+ if (!res.ok)
6931
+ return res;
6932
+ const value = await writeProject(res.value, config2, opts);
6933
+ if (value.installError)
6934
+ (opts.onWarn ?? warn)(value.installError);
6935
+ return { ok: true, value };
4853
6936
  }
6937
+ var warn = (message) => console.error(`! ${message}`);
4854
6938
  var init_create = __esm(() => {
4855
6939
  init_src2();
4856
6940
  init_src3();
@@ -4860,7 +6944,7 @@ var init_create = __esm(() => {
4860
6944
  var exports_mcp = {};
4861
6945
  __export(exports_mcp, {
4862
6946
  startMcpServer: () => startMcpServer,
4863
- planProject: () => planProject,
6947
+ planProject: () => planProject2,
4864
6948
  guidanceText: () => guidanceText,
4865
6949
  createProjectFromInput: () => createProjectFromInput,
4866
6950
  buildMcpServer: () => buildMcpServer
@@ -4868,7 +6952,7 @@ __export(exports_mcp, {
4868
6952
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4869
6953
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4870
6954
  import { z as z3 } from "zod";
4871
- function planProject(input) {
6955
+ function planProject2(input) {
4872
6956
  const config2 = toConfig(input);
4873
6957
  const res = generate(config2, buildRegistry(config2));
4874
6958
  if (!res.ok)
@@ -4886,7 +6970,7 @@ function buildMcpServer() {
4886
6970
  description: "Preview the files a configuration would generate (does NOT write to disk).",
4887
6971
  inputSchema: PlanInput
4888
6972
  }, (input) => {
4889
- const res = planProject(input);
6973
+ const res = planProject2(input);
4890
6974
  return res.ok ? text2(res.paths.join(`
4891
6975
  `)) : { ...text2(`Generation failed: ${res.error}`), isError: true };
4892
6976
  });
@@ -4943,59 +7027,259 @@ var init_mcp = __esm(() => {
4943
7027
  init_src();
4944
7028
  init_package();
4945
7029
  init_create();
7030
+ import { resolve as resolve2 } from "node:path";
7031
+ import { cancel as cancel3, confirm, isCancel, log as log3 } from "@clack/prompts";
4946
7032
  import { initTRPC } from "@trpc/server";
4947
7033
  import { createCli } from "trpc-cli";
4948
7034
  import { z as z2 } from "zod";
4949
7035
 
4950
7036
  // src/prompts.ts
7037
+ init_src3();
4951
7038
  init_src();
4952
- import { cancel, intro, isCancel, multiselect, outro, select, text } from "@clack/prompts";
4953
- var orCancel = (value) => {
4954
- if (isCancel(value)) {
4955
- cancel("Cancelled.");
4956
- process.exit(0);
7039
+ import { cancel, group, groupMultiselect, log, select, text } from "@clack/prompts";
7040
+
7041
+ // src/theme.ts
7042
+ import { WriteStream } from "node:tty";
7043
+ import { isCI, unicode } from "@clack/prompts";
7044
+ var resolveDepth = () => {
7045
+ const stream = process.stdout;
7046
+ if (typeof stream.getColorDepth === "function")
7047
+ return stream.getColorDepth();
7048
+ return process.env.FORCE_COLOR ? WriteStream.prototype.getColorDepth() : 1;
7049
+ };
7050
+ var depth = resolveDepth();
7051
+ var wrap = (open, close) => (value) => `\x1B[${open}m${value}\x1B[${close}m`;
7052
+ var plain = (value) => value;
7053
+ var scale = (rgb, x256, basic) => {
7054
+ if (depth >= 24)
7055
+ return wrap(`38;2;${rgb[0]};${rgb[1]};${rgb[2]}`, "39");
7056
+ if (depth >= 8)
7057
+ return wrap(`38;5;${x256}`, "39");
7058
+ if (depth >= 4)
7059
+ return wrap(String(basic), "39");
7060
+ return plain;
7061
+ };
7062
+ var effect = (open, close) => depth >= 4 ? wrap(open, close) : plain;
7063
+ var accent = scale([124, 140, 255], 105, 36);
7064
+ var accentDim = scale([74, 85, 168], 61, 34);
7065
+ var muted = scale([153, 161, 179], 109, 90);
7066
+ var success = scale([74, 222, 128], 78, 32);
7067
+ var warn2 = scale([251, 191, 36], 214, 33);
7068
+ var danger = scale([248, 113, 113], 203, 31);
7069
+ var bold = effect("1", "22");
7070
+ var dim = effect("2", "22");
7071
+ var badge = (value) => depth >= 24 ? `\x1B[48;2;124;140;255m\x1B[30m ${value} \x1B[39m\x1B[49m` : depth >= 8 ? `\x1B[48;5;105m\x1B[30m ${value} \x1B[39m\x1B[49m` : depth >= 4 ? `\x1B[46m\x1B[30m ${value} \x1B[39m\x1B[49m` : ` ${value} `;
7072
+ var WORDMARK = [
7073
+ "████████╗██╗ ██╗██╗███████╗",
7074
+ "╚══██╔══╝██║ ██║██║╚══███╔╝",
7075
+ " ██║ ██║ █╗ ██║██║ ███╔╝ ",
7076
+ " ██║ ██║███╗██║██║ ███╔╝ ",
7077
+ " ██║ ╚███╔███╔╝██║███████╗",
7078
+ " ╚═╝ ╚══╝╚══╝ ╚═╝╚══════╝"
7079
+ ];
7080
+ var WORDMARK_WIDTH = 30;
7081
+ var TAGLINE = "scaffold a Turborepo monorepo";
7082
+ var shade = (row) => (row.match(/█+|[^█]+/g) ?? []).map((run) => run.startsWith("█") ? accent(run) : accentDim(run)).join("");
7083
+ function banner(version) {
7084
+ if (!process.stdout.isTTY || isCI() || !unicode)
7085
+ return null;
7086
+ const label = `v${version}`;
7087
+ const gap = Math.max(2, WORDMARK_WIDTH - 2 - TAGLINE.length - label.length);
7088
+ const width = Math.max(WORDMARK_WIDTH, 2 + TAGLINE.length + gap + label.length);
7089
+ if ((process.stdout.columns ?? 80) < width + 2)
7090
+ return null;
7091
+ const footer = ` ${muted(TAGLINE)}${" ".repeat(gap)}${dim(label)}`;
7092
+ return `
7093
+ ${WORDMARK.map(shade).join(`
7094
+ `)}
7095
+
7096
+ ${footer}
7097
+ `;
7098
+ }
7099
+
7100
+ // src/prompts.ts
7101
+ var toOption = (entry2) => ({
7102
+ value: entry2.id,
7103
+ label: entry2.label,
7104
+ ...entry2.hint ? { hint: entry2.hint } : {}
7105
+ });
7106
+ var PACKAGE_MANAGER_HINTS = {
7107
+ pnpm: "Strict node_modules, workspace: protocol",
7108
+ bun: "Fastest install, workspace: protocol",
7109
+ npm: "No workspace: protocol — deps are rewritten to *"
7110
+ };
7111
+ var DEPLOY_HINTS = {
7112
+ node: "@astrojs/node — runs anywhere, and what `astro preview` serves",
7113
+ vercel: "@astrojs/vercel — required once a server route deploys to Vercel"
7114
+ };
7115
+ var CATEGORY_LABELS = {
7116
+ ui: "UI",
7117
+ integration: "Integrations",
7118
+ devtools: "Developer tooling",
7119
+ "cross-cutting": "Cross-cutting"
7120
+ };
7121
+ var moduleGroups = () => {
7122
+ const catalog = moduleCatalog();
7123
+ const groups = {};
7124
+ for (const category of MODULE_CATEGORIES) {
7125
+ const entries = catalog.filter((entry2) => entry2.category === category);
7126
+ if (entries.length)
7127
+ groups[CATEGORY_LABELS[category]] = entries.map(toOption);
4957
7128
  }
4958
- return value;
7129
+ return groups;
7130
+ };
7131
+ var bail = () => {
7132
+ cancel("Cancelled.");
7133
+ process.exit(0);
4959
7134
  };
4960
7135
  async function gatherConfig(flags) {
4961
- intro("create-turbo-wizard");
4962
- const name = flags.name ?? orCancel(await text({
4963
- message: "Project name",
4964
- placeholder: "my-app",
4965
- defaultValue: "my-app",
4966
- validate: (v) => /^[a-z0-9][a-z0-9._-]*$/.test(v || "my-app") ? undefined : "lowercase, digits, -._ only"
4967
- }));
4968
- const frontend = flags.frontend ?? orCancel(await select({
4969
- message: "Frontend framework",
4970
- options: [
4971
- { value: "next", label: "Next.js" },
4972
- { value: "astro", label: "Astro" }
4973
- ]
4974
- }));
4975
- const content = flags.content ?? orCancel(await select({
4976
- message: "Content source",
4977
- options: CONTENT_SOURCES.map((value) => ({ value, label: value }))
4978
- }));
4979
- const packageManager = flags.packageManager ?? orCancel(await select({
4980
- message: "Package manager",
4981
- options: PACKAGE_MANAGERS.map((value) => ({ value, label: value }))
4982
- }));
4983
- const modules = flags.modules ?? orCancel(await multiselect({
4984
- message: "Optional modules (space to toggle)",
4985
- options: MODULES.map((value) => ({ value, label: value })),
4986
- required: false
4987
- }));
4988
- outro(`Scaffolding ${name} (${frontend} · ${content} · ${packageManager})`);
7136
+ const answers = await group({
7137
+ name: () => flags.name ? Promise.resolve(flags.name) : text({
7138
+ message: "Project name",
7139
+ placeholder: "my-app",
7140
+ defaultValue: "my-app",
7141
+ validate: (value) => {
7142
+ const parsed = ProjectNameSchema.safeParse(value || "my-app");
7143
+ return parsed.success ? undefined : parsed.error.issues[0]?.message;
7144
+ }
7145
+ }),
7146
+ frontend: () => flags.frontend ? Promise.resolve(flags.frontend) : select({
7147
+ message: "Frontend framework",
7148
+ options: frontendCatalog().map(toOption),
7149
+ initialValue: "next"
7150
+ }),
7151
+ content: () => flags.content ? Promise.resolve(flags.content) : select({
7152
+ message: "Content source",
7153
+ options: contentCatalog().map(toOption)
7154
+ }),
7155
+ packageManager: () => flags.packageManager ? Promise.resolve(flags.packageManager) : select({
7156
+ message: "Package manager",
7157
+ options: PACKAGE_MANAGERS.map((value) => ({
7158
+ value,
7159
+ label: value,
7160
+ hint: PACKAGE_MANAGER_HINTS[value]
7161
+ }))
7162
+ }),
7163
+ deploy: ({ results }) => flags.deploy || results.frontend !== "astro" ? Promise.resolve(flags.deploy ?? "node") : select({
7164
+ message: "Deploy target",
7165
+ options: DEPLOY_TARGETS.map((value) => ({
7166
+ value,
7167
+ label: value,
7168
+ hint: DEPLOY_HINTS[value]
7169
+ }))
7170
+ }),
7171
+ modules: () => flags.modules ? Promise.resolve(flags.modules) : groupMultiselect({
7172
+ message: `Modules ${muted("(space to toggle, enter to confirm)")}`,
7173
+ options: moduleGroups(),
7174
+ required: false
7175
+ })
7176
+ }, { onCancel: bail });
7177
+ const picked = (answers.modules ?? []).filter((id) => MODULES.includes(id));
7178
+ const resolved = flags.modules ? { modules: picked, added: [] } : withPrerequisites(picked);
7179
+ for (const [module, requiredBy] of resolved.added) {
7180
+ log.info(`Added ${accent(module)} — required by ${accent(requiredBy)}`);
7181
+ }
4989
7182
  return WizardConfigSchema.parse({
4990
- name,
4991
- frontend,
4992
- content,
4993
- packageManager,
4994
- modules,
7183
+ name: answers.name,
7184
+ frontend: answers.frontend,
7185
+ content: answers.content,
7186
+ packageManager: answers.packageManager,
7187
+ deploy: answers.deploy,
7188
+ modules: resolved.modules,
4995
7189
  experimental: { cacheComponents: flags.cacheComponents, reactCompiler: flags.reactCompiler }
4996
7190
  });
4997
7191
  }
4998
7192
 
7193
+ // src/ui.ts
7194
+ init_src3();
7195
+ init_src();
7196
+ import { relative, sep } from "node:path";
7197
+ import { cancel as cancel2, intro, log as log2, note, outro, taskLog } from "@clack/prompts";
7198
+ var RAW = { format: (line) => line };
7199
+ function printBanner(version) {
7200
+ const art = banner(version);
7201
+ if (art)
7202
+ process.stdout.write(`${art}
7203
+ `);
7204
+ }
7205
+ function start() {
7206
+ intro(badge("create-turbo-wizard"));
7207
+ }
7208
+ function displayPath(dir) {
7209
+ const rel = relative(process.cwd(), dir);
7210
+ return rel && !rel.startsWith("..") && !rel.startsWith(sep) ? `./${rel}` : dir;
7211
+ }
7212
+ function shape(tree) {
7213
+ const nested = new Map;
7214
+ let rootFiles = 0;
7215
+ for (const path of tree.keys()) {
7216
+ const [top, second] = path.split("/");
7217
+ if (!top)
7218
+ continue;
7219
+ if (second && (top === "apps" || top === "packages")) {
7220
+ const names = nested.get(top) ?? new Set;
7221
+ names.add(second);
7222
+ nested.set(top, names);
7223
+ } else if (!second) {
7224
+ rootFiles += 1;
7225
+ }
7226
+ }
7227
+ const parts = [...nested].sort(([a], [b]) => a.localeCompare(b)).map(([top, names]) => names.size > 3 ? `${names.size} ${top}` : `${top}/{${[...names].sort().join(", ")}}`);
7228
+ if (rootFiles)
7229
+ parts.push(`${rootFiles} root files`);
7230
+ return parts.join(" ");
7231
+ }
7232
+ var labelOf = (entries, id) => entries.find((entry2) => entry2.id === id)?.label ?? id;
7233
+ var row = (label, value) => `${muted(label.padEnd(10))}${value}`;
7234
+ function summary(config2, tree, dir) {
7235
+ const stack = [
7236
+ labelOf(frontendCatalog(), config2.frontend),
7237
+ labelOf(contentCatalog(), config2.content),
7238
+ config2.packageManager
7239
+ ].join(" · ");
7240
+ note([
7241
+ row("Directory", accent(displayPath(dir))),
7242
+ row("Stack", stack),
7243
+ row("Modules", config2.modules.length ? config2.modules.join(", ") : muted("none")),
7244
+ row("Writing", `${tree.size} files ${muted(shape(tree))}`),
7245
+ "",
7246
+ muted("Reproduce this with"),
7247
+ dim(buildCommand(config2))
7248
+ ].join(`
7249
+ `), "Summary", RAW);
7250
+ }
7251
+ function installLog(config2) {
7252
+ const task = taskLog({
7253
+ title: `Installing dependencies with ${config2.packageManager}`,
7254
+ limit: 8
7255
+ });
7256
+ return {
7257
+ onOutput: (line) => task.message(line, { raw: true }),
7258
+ finish: (error) => error ? task.error(error, { showLog: true }) : task.success(`${config2.packageManager} install`)
7259
+ };
7260
+ }
7261
+ var runScript = (config2, script) => config2.packageManager === "npm" ? `npm run ${script}` : `${config2.packageManager} ${script}`;
7262
+ function nextSteps(config2, tree, dir, installed) {
7263
+ const commands = [`cd ${displayPath(dir)}`];
7264
+ if (!installed)
7265
+ commands.push(`${config2.packageManager} install`);
7266
+ commands.push(runScript(config2, "dev"));
7267
+ if (config2.content === "sanity")
7268
+ commands.push(runScript(config2, "dev:studio"));
7269
+ const extras = [];
7270
+ if (tree.has(".env.example"))
7271
+ extras.push(row("Env", "copy .env.example to .env and fill it in"));
7272
+ extras.push(row("Docs", "https://turbo-wizard.vercel.app/docs"));
7273
+ note([...commands.map(accent), "", ...extras].join(`
7274
+ `), "Next steps", RAW);
7275
+ outro(success(`Created ${config2.name} — ${tree.size} files`));
7276
+ }
7277
+ function fail(message) {
7278
+ log2.error(message);
7279
+ cancel2("Nothing was written.");
7280
+ process.exit(1);
7281
+ }
7282
+
4999
7283
  // src/router.ts
5000
7284
  var t = initTRPC.create();
5001
7285
  var CreateInput = z2.object({
@@ -5003,6 +7287,7 @@ var CreateInput = z2.object({
5003
7287
  frontend: z2.enum(FRONTENDS).optional(),
5004
7288
  content: z2.enum(CONTENT_SOURCES).optional(),
5005
7289
  packageManager: z2.enum(PACKAGE_MANAGERS).optional(),
7290
+ deploy: z2.enum(DEPLOY_TARGETS).optional(),
5006
7291
  modules: z2.array(z2.enum(MODULES)).optional(),
5007
7292
  preset: z2.enum(PRESETS).optional(),
5008
7293
  cacheComponents: z2.boolean().optional(),
@@ -5020,33 +7305,48 @@ var router = t.router({
5020
7305
  frontend: input.frontend,
5021
7306
  content: input.content ?? base.content,
5022
7307
  packageManager: input.packageManager ?? base.packageManager,
7308
+ deploy: input.deploy ?? base.deploy,
5023
7309
  modules: input.modules ?? base.modules,
5024
7310
  cacheComponents: input.cacheComponents ?? base.experimental?.cacheComponents,
5025
7311
  reactCompiler: input.reactCompiler ?? base.experimental?.reactCompiler
5026
7312
  };
7313
+ printBanner(package_default.version);
7314
+ start();
5027
7315
  const interactive = !input.yes && !input.preset && Boolean(process.stdout.isTTY) && (!input.frontend || !input.name);
5028
7316
  const config2 = interactive ? await gatherConfig(flags) : WizardConfigSchema.parse({
5029
7317
  name: flags.name ?? "my-app",
5030
7318
  frontend: flags.frontend ?? "next",
5031
7319
  content: flags.content,
5032
7320
  packageManager: flags.packageManager,
7321
+ deploy: flags.deploy,
5033
7322
  modules: flags.modules,
5034
7323
  experimental: {
5035
7324
  cacheComponents: flags.cacheComponents,
5036
7325
  reactCompiler: flags.reactCompiler
5037
7326
  }
5038
7327
  });
5039
- const res = await createProject(config2, {
5040
- targetDir: input.dir,
7328
+ const plan = planProject(config2);
7329
+ if (!plan.ok)
7330
+ fail(plan.error.message);
7331
+ const dir = resolve2(input.dir ?? config2.name);
7332
+ summary(config2, plan.value, dir);
7333
+ if (interactive) {
7334
+ const proceed = await confirm({ message: "Proceed with scaffolding?" });
7335
+ if (isCancel(proceed) || !proceed) {
7336
+ cancel3("Nothing was written.");
7337
+ process.exit(0);
7338
+ }
7339
+ }
7340
+ const installer = input.install ? installLog(config2) : undefined;
7341
+ const result = await writeProject(plan.value, config2, {
7342
+ targetDir: dir,
5041
7343
  install: input.install,
5042
- git: input.git
7344
+ git: input.git,
7345
+ onWarn: (message) => log3.warn(message),
7346
+ onOutput: installer?.onOutput
5043
7347
  });
5044
- if (!res.ok) {
5045
- console.error(`✗ ${res.error.message}`);
5046
- process.exit(1);
5047
- }
5048
- console.error(`✓ created ${config2.name} → ${res.value.dir} (${res.value.fileCount} files)`);
5049
- console.error(` next: cd ${res.value.dir} && ${config2.packageManager} install`);
7348
+ installer?.finish(result.installError);
7349
+ nextSteps(config2, plan.value, result.dir, result.installError === null);
5050
7350
  })
5051
7351
  });
5052
7352
  var cli = createCli({ router, name: "create-turbo-wizard", version: package_default.version });