From 0c603bf4cbe72730ba1c011fbc78b88c40a62d2f Mon Sep 17 00:00:00 2001 From: Josh Date: Thu, 19 Mar 2026 15:07:24 -0700 Subject: [PATCH] chore(sdk): update SDK skill for better onboarding experience --- .../references/app-sdk.md | 126 ++++++++++-------- 1 file changed, 73 insertions(+), 53 deletions(-) diff --git a/skills/sanity-best-practices/references/app-sdk.md b/skills/sanity-best-practices/references/app-sdk.md index 99856af..dad80fb 100644 --- a/skills/sanity-best-practices/references/app-sdk.md +++ b/skills/sanity-best-practices/references/app-sdk.md @@ -1,17 +1,37 @@ --- title: Sanity App SDK -description: Rules for building custom applications with the Sanity App SDK, including React hooks, document handles, real-time patterns, and Suspense best practices. +description: Rules for building custom applications with the Sanity App SDK, including React hooks, document handles, real-time patterns, and Suspense best practices. Use these rules when users ask to 'Make an SDK app' or 'Make a sanity application'. --- # Sanity App SDK Build custom React applications that interact with Sanity content in real-time. +## Before Scaffolding + +Before running any CLI command or writing any code: + +1. **Organization** — call `list_organizations`. For each org returned, call + `get_current_user_resource_permissions` with `resourceType: "organization"`, + `resourceId: `, and `permissions: ["sanity.sdk.applications.deploy"]`. + Only present orgs where that permission is granted. If none qualify, stop and + tell the user they don't have permission to deploy Sanity apps. + +2. **Project** — call `list_projects` with `organizationId: ` and + `onlyExplicitMembership: true`. For the user's chosen project, call + `get_current_user_resource_permissions` with `resourceType: "project"`, + `resourceId: `, and `permissions: ["sanity.project.read"]`. + If they have no projects or want a new one, offer to call `create_project`. + +3. **Template** — ask: basic quickstart, or with Sanity UI? + +Do not proceed until `organizationId` and `projectId` are confirmed. + ## Tech Stack - **Framework:** React 19+, TypeScript -- **Packages:** `@sanity/sdk`, `@sanity/sdk-react` -- **Optional UI:** `@sanity/ui`, `styled-components` +- **Packages:** `@sanity/sdk`, `@sanity/sdk-react` – make sure you always use the latest versions +- **Optional UI:** `@sanity/ui`, `styled-components` – make sure you always use the latest versions - **Runtime:** Node.js 20+ ## Commands @@ -63,14 +83,14 @@ my-app/ ### CLI Config (`sanity.cli.ts`) ```typescript -import { defineCliConfig } from 'sanity/cli' +import { defineCliConfig } from "sanity/cli"; export default defineCliConfig({ app: { - organizationId: 'your-org-id', - entry: './src/App.tsx', + organizationId: "your-org-id", + entry: "./src/App.tsx", }, -}) +}); ``` ### App Root (`src/App.tsx`) @@ -137,10 +157,10 @@ Lightweight references to documents. Fetch handles first, then load content as n ```typescript interface DocumentHandle { - documentId: string - documentType: string - projectId?: string - dataset?: string + documentId: string; + documentType: string; + projectId?: string; + dataset?: string; } ``` @@ -148,33 +168,33 @@ interface DocumentHandle { ```typescript // Best: From useDocuments hook -const { data: handles } = useDocuments({ documentType: 'article' }) +const { data: handles } = useDocuments({ documentType: "article" }); // Good: With helper (preserves literal types for TypeGen) -import { createDocumentHandle } from '@sanity/sdk' +import { createDocumentHandle } from "@sanity/sdk"; const handle = createDocumentHandle({ - documentId: 'my-doc-id', - documentType: 'article', -}) + documentId: "my-doc-id", + documentType: "article", +}); // Good: With as const (preserves literal types) const handle = { - documentId: 'my-doc-id', - documentType: 'article', -} as const + documentId: "my-doc-id", + documentType: "article", +} as const; ``` --- ## Hook Selection -| Hook | Use Case | Returns | -|------|----------|---------| -| `useDocuments` | List of documents (infinite scroll) | Document handles | -| `usePaginatedDocuments` | Paginated lists with page controls | Document handles | -| `useDocument` | Single document, real-time editing | Full document or field | -| `useDocumentProjection` | Specific fields, display only | Projected data | -| `useQuery` | Complex GROQ queries (use sparingly) | Raw query results | +| Hook | Use Case | Returns | +| ----------------------- | ------------------------------------ | ---------------------- | +| `useDocuments` | List of documents (infinite scroll) | Document handles | +| `usePaginatedDocuments` | Paginated lists with page controls | Document handles | +| `useDocument` | Single document, real-time editing | Full document or field | +| `useDocumentProjection` | Specific fields, display only | Projected data | +| `useQuery` | Complex GROQ queries (use sparingly) | Raw query results | --- @@ -348,8 +368,8 @@ function VenuesList() { ```typescript // Bad: Multiple fetchers in one component function BadComponent() { - const { data: events } = useDocuments({ documentType: 'event' }) - const { data: venues } = useDocuments({ documentType: 'venue' }) + const { data: events } = useDocuments({ documentType: "event" }); + const { data: venues } = useDocuments({ documentType: "venue" }); // Both trigger Suspense together, causing unnecessary re-renders } ``` @@ -379,27 +399,27 @@ function OpenInStudioButton({ handle }: { handle: DocumentHandle }) { ## Event Handling ```typescript -import { useDocumentEvent, DocumentEvent } from '@sanity/sdk-react' +import { useDocumentEvent, DocumentEvent } from "@sanity/sdk-react"; function DocumentWatcher(handle: DocumentHandle) { useDocumentEvent({ ...handle, onEvent: (event) => { switch (event.type) { - case 'edited': - console.log('Edited:', event.documentId) - break - case 'published': - console.log('Published:', event.documentId) - break - case 'deleted': - console.log('Deleted:', event.documentId) - break + case "edited": + console.log("Edited:", event.documentId); + break; + case "published": + console.log("Published:", event.documentId); + break; + case "deleted": + console.log("Deleted:", event.documentId); + break; } }, - }) + }); - return null + return null; } ``` @@ -409,17 +429,17 @@ function DocumentWatcher(handle: DocumentHandle) { ```typescript const config: SanityConfig[] = [ - { projectId: 'project-1', dataset: 'production' }, - { projectId: 'project-2', dataset: 'staging' }, -] + { projectId: "project-1", dataset: "production" }, + { projectId: "project-2", dataset: "staging" }, +]; // Handles include project/dataset info const handle: DocumentHandle = { - documentId: 'doc-123', - documentType: 'article', - projectId: 'project-1', - dataset: 'production', -} + documentId: "doc-123", + documentType: "article", + projectId: "project-1", + dataset: "production", +}; ``` --- @@ -455,8 +475,8 @@ The App SDK provides hooks and data stores. You bring: ## Troubleshooting -| Issue | Solution | -|-------|----------| -| Safari dev issues | Use Chrome or Firefox during development | -| Port 3333 in use | `npm run dev -- --port 3334` | -| Auth errors | `npx sanity@latest logout && npx sanity@latest login` | +| Issue | Solution | +| ----------------- | ----------------------------------------------------- | +| Safari dev issues | Use Chrome or Firefox during development | +| Port 3333 in use | `npm run dev -- --port 3334` | +| Auth errors | `npx sanity@latest logout && npx sanity@latest login` |