bsmnt 0.0.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.
- package/.changeset/2026-02-11-test-patch-bump.md +5 -0
- package/.changeset/README.md +10 -0
- package/.changeset/config.json +16 -0
- package/.cursor/rules/README.md +184 -0
- package/.cursor/rules/architecture.mdc +437 -0
- package/.cursor/rules/components.mdc +436 -0
- package/.cursor/rules/integrations.mdc +447 -0
- package/.cursor/rules/main.mdc +278 -0
- package/.cursor/rules/styling.mdc +433 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +14 -0
- package/.github/workflows/.gitkeep +0 -0
- package/.github/workflows/ci.yml +37 -0
- package/.github/workflows/release.yml +54 -0
- package/.tldr/cache/call_graph.json +7 -0
- package/.tldr/languages.json +6 -0
- package/.tldr/status +1 -0
- package/.tldrignore +84 -0
- package/.vscode/extensions.json +20 -0
- package/.vscode/settings.json +98 -0
- package/CHANGELOG.md +13 -0
- package/CLAUDE.md +138 -0
- package/README.md +176 -0
- package/bin/index.js +262 -0
- package/biome.json +44 -0
- package/bun.lock +496 -0
- package/changelog/04-02-26.md +86 -0
- package/changelog/05-02-26.md +101 -0
- package/changelog/09-02-26.md +83 -0
- package/docs/fix-studio-hydration.md +46 -0
- package/docs/plans/2026-01-29-sanity-smart-merge-design.md +196 -0
- package/docs/plans/2026-01-29-sanity-smart-merge-implementation.md +695 -0
- package/docs/sanity-setup-steps.md +199 -0
- package/integrations/basehub/README.md +3 -0
- package/integrations/sanity/app/api/draft-mode/disable/route.ts +7 -0
- package/integrations/sanity/app/api/draft-mode/enable/route.ts +21 -0
- package/integrations/sanity/app/api/revalidate/route.ts +37 -0
- package/integrations/sanity/app/layout.tsx +111 -0
- package/integrations/sanity/app/sitemap.ts +80 -0
- package/integrations/sanity/app/studio/[[...tool]]/page.tsx +8 -0
- package/integrations/sanity/app/studio/layout.tsx +7 -0
- package/integrations/sanity/components/ui/sanity-image/index.tsx +37 -0
- package/integrations/sanity/lib/integrations/README.md +58 -0
- package/integrations/sanity/lib/integrations/check-integration.ts +62 -0
- package/integrations/sanity/lib/integrations/sanity/README.md +144 -0
- package/integrations/sanity/lib/integrations/sanity/client.ts +30 -0
- package/integrations/sanity/lib/integrations/sanity/components/disable-draft-mode.tsx +29 -0
- package/integrations/sanity/lib/integrations/sanity/components/rich-text.tsx +73 -0
- package/integrations/sanity/lib/integrations/sanity/env.ts +38 -0
- package/integrations/sanity/lib/integrations/sanity/live/index.tsx +34 -0
- package/integrations/sanity/lib/integrations/sanity/queries.ts +99 -0
- package/integrations/sanity/lib/integrations/sanity/sanity.cli.ts +20 -0
- package/integrations/sanity/lib/integrations/sanity/sanity.config.ts +94 -0
- package/integrations/sanity/lib/integrations/sanity/sanity.types.ts +337 -0
- package/integrations/sanity/lib/integrations/sanity/schema.json +1850 -0
- package/integrations/sanity/lib/integrations/sanity/schemas/article.ts +132 -0
- package/integrations/sanity/lib/integrations/sanity/schemas/example.ts +203 -0
- package/integrations/sanity/lib/integrations/sanity/schemas/index.ts +37 -0
- package/integrations/sanity/lib/integrations/sanity/schemas/link.ts +127 -0
- package/integrations/sanity/lib/integrations/sanity/schemas/metadata.ts +68 -0
- package/integrations/sanity/lib/integrations/sanity/schemas/navigation.ts +39 -0
- package/integrations/sanity/lib/integrations/sanity/schemas/page.ts +77 -0
- package/integrations/sanity/lib/integrations/sanity/schemas/richText.ts +59 -0
- package/integrations/sanity/lib/integrations/sanity/structure.ts +5 -0
- package/integrations/sanity/lib/integrations/sanity/utils/image.ts +11 -0
- package/integrations/sanity/lib/integrations/sanity/utils/link.ts +61 -0
- package/integrations/sanity/lib/scripts/copy-sanity-mcp.ts +23 -0
- package/integrations/sanity/lib/scripts/generate-page.ts +310 -0
- package/integrations/sanity/lib/utils/metadata.ts +190 -0
- package/layers/experiment/components/layout/header/index.tsx +58 -0
- package/layers/experiment/components/layout/navigation-menu.tsx +127 -0
- package/layers/experiment/lib/constants.ts +12 -0
- package/layers/webgl/app/page.tsx +10 -0
- package/layers/webgl/components/webgl/canvas/dynamic.tsx +34 -0
- package/layers/webgl/components/webgl/canvas/index.tsx +43 -0
- package/layers/webgl/components/webgl/components/scene/index.tsx +21 -0
- package/layers/webgpu/.gitkeep +0 -0
- package/package.json +44 -0
- package/plugins/README.md +21 -0
- package/plugins/no-anchor-element.grit +11 -0
- package/plugins/no-relative-parent-imports.grit +6 -0
- package/plugins/no-unnecessary-forwardref.grit +5 -0
- package/src/commands/add-integration.js +325 -0
- package/src/commands/create.js +415 -0
- package/src/commands/setup-sanity.js +426 -0
- package/src/commands/worktree.js +805 -0
- package/src/mergers/check-integration-merger.js +105 -0
- package/src/mergers/config.js +137 -0
- package/src/mergers/index.js +355 -0
- package/src/mergers/layout-merger.js +223 -0
- package/src/mergers/next-config-merger.js +63 -0
- package/src/mergers/sitemap-merger.js +121 -0
- package/tasks/prd-next-starter-dynamic-layers.md +184 -0
- package/tasks/prd.json +153 -0
- package/tasks/progress.txt +115 -0
- package/template-hooks/use-battery.ts +126 -0
- package/template-hooks/use-device-perf.ts +184 -0
- package/template-hooks/use-intersection-observer.ts +32 -0
- package/template-hooks/use-media.ts +33 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# Sanity Project Setup — Automated Steps
|
|
2
|
+
|
|
3
|
+
> Reproducible steps for programmatically creating a Sanity project, dataset, API token, and `.env.local` configuration. No browser or interactive prompts required.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- `@sanity/cli` available via `npx` (comes with `sanity` package)
|
|
10
|
+
- User must be logged in to Sanity CLI (`npx sanity login`)
|
|
11
|
+
- Project already has `next-sanity`, `@sanity/client`, `sanity` installed
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Step 1: Verify Sanity CLI is available
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx sanity --version
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
If this fails, the sanity packages aren't installed. Run:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
bun add next-sanity @sanity/client @sanity/image-url sanity @sanity/vision
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Step 2: Verify user is authenticated
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npx sanity projects list
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
- If this returns a list of projects, the user is logged in.
|
|
36
|
+
- If it fails with an auth error, the user needs to run `npx sanity login` first (this is interactive and opens a browser — cannot be automated).
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Step 3: Create the Sanity project
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npx sanity projects create --name "<project-name>" --dataset production
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**Output to parse:**
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
Project created successfully!
|
|
50
|
+
ID: <project-id>
|
|
51
|
+
Name: <project-name>
|
|
52
|
+
Organization: <org>
|
|
53
|
+
Dataset: production (public)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Extract the project ID** from the output. It's the alphanumeric string after `ID: ` (e.g., `1poovu2i`).
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Step 4: Get the user's auth token
|
|
61
|
+
|
|
62
|
+
The Sanity CLI stores the user's session token locally. Retrieve it with:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npx sanity debug --secrets 2>&1 | grep "Auth token"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Output:**
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
Auth token: '<token-string>'
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Parse the token** from between the single quotes. Note: the output includes ANSI color codes (`[32m` and `[39m`) that need to be stripped.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Step 5: Create an API read token
|
|
79
|
+
|
|
80
|
+
Use the Sanity HTTP API to create a viewer token for the project:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
curl -s -X POST "https://api.sanity.io/v2021-06-07/projects/<project-id>/tokens" \
|
|
84
|
+
-H "Authorization: Bearer <auth-token-from-step-4>" \
|
|
85
|
+
-H "Content-Type: application/json" \
|
|
86
|
+
-d '{"label": "Next.js Read Token", "roleName": "viewer"}'
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Response:**
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"id": "si8WgRxQuts4CN",
|
|
94
|
+
"key": "<the-api-read-token>",
|
|
95
|
+
"roles": [{ "name": "viewer", "title": "Viewer" }],
|
|
96
|
+
"label": "Next.js Read Token"
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Extract the `key` field** — this is the `SANITY_API_READ_TOKEN`.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Step 6: Write `.env.local`
|
|
105
|
+
|
|
106
|
+
Create `.env.local` in the project root with:
|
|
107
|
+
|
|
108
|
+
```env
|
|
109
|
+
# Sanity CMS
|
|
110
|
+
NEXT_PUBLIC_SANITY_PROJECT_ID=<project-id>
|
|
111
|
+
NEXT_PUBLIC_SANITY_DATASET=production
|
|
112
|
+
SANITY_API_READ_TOKEN=<api-read-token>
|
|
113
|
+
NEXT_PUBLIC_SANITY_API_READ_TOKEN=<api-read-token>
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Why both `SANITY_API_READ_TOKEN` and `NEXT_PUBLIC_SANITY_API_READ_TOKEN`?**
|
|
117
|
+
|
|
118
|
+
The next-starter template's `env.ts` reads from `NEXT_PUBLIC_SANITY_API_READ_TOKEN` (exposed to client for draft mode / visual editing). `SANITY_API_READ_TOKEN` is the conventional server-only name. Setting both ensures compatibility regardless of which convention the project follows.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Step 7: Verify the connection
|
|
123
|
+
|
|
124
|
+
Test that the token and project ID work by querying the API:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
curl -s "https://<project-id>.api.sanity.io/v2024-03-15/data/query/production?query=*%5B0%5D" \
|
|
128
|
+
-H "Authorization: Bearer <api-read-token>"
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Success response:**
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{
|
|
135
|
+
"query": "*[0]",
|
|
136
|
+
"result": { ... },
|
|
137
|
+
"ms": 2
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
If the response contains `"result"` (even if it's `null` for an empty dataset), the setup is correct.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Step 8: Ensure `.env.local` is gitignored
|
|
146
|
+
|
|
147
|
+
Check if `.gitignore` exists and includes `.env.local`. If not, add it:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
# Check
|
|
151
|
+
grep -q ".env.local" .gitignore 2>/dev/null
|
|
152
|
+
|
|
153
|
+
# If missing or .gitignore doesn't exist, ensure it's added
|
|
154
|
+
echo ".env.local" >> .gitignore
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Summary of values produced
|
|
160
|
+
|
|
161
|
+
| Variable | Source |
|
|
162
|
+
|----------|--------|
|
|
163
|
+
| `NEXT_PUBLIC_SANITY_PROJECT_ID` | Step 3 — `ID` field from project creation output |
|
|
164
|
+
| `NEXT_PUBLIC_SANITY_DATASET` | Hardcoded to `production` (passed in Step 3) |
|
|
165
|
+
| `SANITY_API_READ_TOKEN` | Step 5 — `key` field from token creation response |
|
|
166
|
+
| `NEXT_PUBLIC_SANITY_API_READ_TOKEN` | Same as above |
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Error handling notes
|
|
171
|
+
|
|
172
|
+
| Step | Possible failure | How to handle |
|
|
173
|
+
|------|-----------------|---------------|
|
|
174
|
+
| 2 | Not logged in | Prompt user to run `npx sanity login` — requires browser |
|
|
175
|
+
| 3 | Org selection needed | Add `--organization <org-id>` flag if user has multiple orgs |
|
|
176
|
+
| 4 | Token not found | Auth token location varies by OS. Fallback: read `~/.config/sanity/config.json` |
|
|
177
|
+
| 5 | 401/403 from API | Auth token expired or invalid — user needs to `npx sanity login` again |
|
|
178
|
+
| 5 | 400 bad request | Check project ID is correct, check auth header format |
|
|
179
|
+
| 7 | Connection refused | Project ID or dataset name is wrong |
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## CLI integration notes
|
|
184
|
+
|
|
185
|
+
For a `basement init` command, the flow would be:
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
1. Check: `npx sanity projects list` → confirms auth
|
|
189
|
+
2. Prompt: "Project name?" (default: directory name)
|
|
190
|
+
3. Run: `npx sanity projects create --name <name> --dataset production`
|
|
191
|
+
4. Parse: project ID from stdout
|
|
192
|
+
5. Get: auth token from `npx sanity debug --secrets`
|
|
193
|
+
6. Create: API token via HTTP POST
|
|
194
|
+
7. Write: `.env.local` with all four variables
|
|
195
|
+
8. Verify: curl test query
|
|
196
|
+
9. Print: "Sanity Studio available at /studio"
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
No interactive Sanity prompts needed. The only prerequisite is that `npx sanity login` has been run at least once.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { defineEnableDraftMode } from "next-sanity/draft-mode";
|
|
3
|
+
import { isSanityConfigured } from "@/lib/integrations/check-integration";
|
|
4
|
+
import { client } from "@/lib/integrations/sanity/client";
|
|
5
|
+
import { privateToken } from "@/lib/integrations/sanity/env";
|
|
6
|
+
|
|
7
|
+
// Only enable draft mode if Sanity is configured
|
|
8
|
+
const draftModeHandler =
|
|
9
|
+
isSanityConfigured() && client
|
|
10
|
+
? defineEnableDraftMode({
|
|
11
|
+
client: client.withConfig({ token: privateToken }),
|
|
12
|
+
})
|
|
13
|
+
: {
|
|
14
|
+
GET: () =>
|
|
15
|
+
NextResponse.json(
|
|
16
|
+
{ error: "Sanity is not configured" },
|
|
17
|
+
{ status: 503 },
|
|
18
|
+
),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const { GET } = draftModeHandler;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { revalidateTag } from "next/cache";
|
|
2
|
+
import { type NextRequest, NextResponse } from "next/server";
|
|
3
|
+
import { parseBody } from "next-sanity/webhook";
|
|
4
|
+
|
|
5
|
+
export async function POST(request: NextRequest) {
|
|
6
|
+
try {
|
|
7
|
+
const { body, isValidSignature } = await parseBody<{
|
|
8
|
+
_type: string;
|
|
9
|
+
slug?: { current: string };
|
|
10
|
+
}>(request, process.env.SANITY_REVALIDATE_SECRET);
|
|
11
|
+
|
|
12
|
+
if (!isValidSignature) {
|
|
13
|
+
return new Response("Invalid signature", { status: 401 });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (!body?._type) {
|
|
17
|
+
return new Response("Bad Request", { status: 400 });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Revalidate the specific document type
|
|
21
|
+
revalidateTag(body._type);
|
|
22
|
+
|
|
23
|
+
// If there's a slug, revalidate the specific page
|
|
24
|
+
if (body.slug?.current) {
|
|
25
|
+
revalidateTag(`${body._type}:${body.slug.current}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return NextResponse.json({
|
|
29
|
+
status: 200,
|
|
30
|
+
revalidated: true,
|
|
31
|
+
now: Date.now(),
|
|
32
|
+
});
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error("Revalidation error:", error);
|
|
35
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { Metadata, Viewport } from "next";
|
|
2
|
+
import { Geist } from "next/font/google";
|
|
3
|
+
import { type PropsWithChildren, Suspense } from "react";
|
|
4
|
+
import { SanityStudioGuard } from "@/components/sanity/studio-guard";
|
|
5
|
+
import { SanityVisualEditing } from "@/components/sanity/visual-editing";
|
|
6
|
+
import { Link } from "@/components/ui/link";
|
|
7
|
+
import { themes } from "@/lib/styles/colors";
|
|
8
|
+
import { fontsVariable } from "@/lib/styles/fonts";
|
|
9
|
+
import AppData from "@/package.json";
|
|
10
|
+
import "@/lib/styles/css/index.css";
|
|
11
|
+
import { cn } from "@/lib/styles/cn";
|
|
12
|
+
|
|
13
|
+
const APP_NAME = AppData.name;
|
|
14
|
+
const APP_DEFAULT_TITLE = "Basement Starter";
|
|
15
|
+
const APP_TITLE_TEMPLATE = "%s - Basement Starter";
|
|
16
|
+
const APP_DESCRIPTION = AppData.description;
|
|
17
|
+
const APP_BASE_URL =
|
|
18
|
+
process.env.NEXT_PUBLIC_BASE_URL ?? "https://localhost:3000";
|
|
19
|
+
|
|
20
|
+
const geist = Geist({
|
|
21
|
+
subsets: ["latin"],
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const metadata: Metadata = {
|
|
25
|
+
alternates: {
|
|
26
|
+
canonical: "/",
|
|
27
|
+
languages: {
|
|
28
|
+
"en-US": "/en-US",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
appleWebApp: {
|
|
32
|
+
capable: true,
|
|
33
|
+
statusBarStyle: "default",
|
|
34
|
+
title: APP_DEFAULT_TITLE,
|
|
35
|
+
},
|
|
36
|
+
applicationName: APP_NAME,
|
|
37
|
+
authors: [{ name: "basement.studio", url: "https://basement.studio" }],
|
|
38
|
+
description: APP_DESCRIPTION,
|
|
39
|
+
formatDetection: { telephone: false },
|
|
40
|
+
metadataBase: new URL(APP_BASE_URL),
|
|
41
|
+
openGraph: {
|
|
42
|
+
description: APP_DESCRIPTION,
|
|
43
|
+
images: [
|
|
44
|
+
{
|
|
45
|
+
alt: APP_DEFAULT_TITLE,
|
|
46
|
+
height: 630,
|
|
47
|
+
url: "/opengraph-image.jpg",
|
|
48
|
+
width: 1200,
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
locale: "en_US",
|
|
52
|
+
siteName: APP_NAME,
|
|
53
|
+
title: {
|
|
54
|
+
default: APP_DEFAULT_TITLE,
|
|
55
|
+
template: APP_TITLE_TEMPLATE,
|
|
56
|
+
},
|
|
57
|
+
type: "website",
|
|
58
|
+
url: APP_BASE_URL,
|
|
59
|
+
},
|
|
60
|
+
other: {
|
|
61
|
+
"fb:app_id": process.env.NEXT_PUBLIC_FACEBOOK_APP_ID || "",
|
|
62
|
+
},
|
|
63
|
+
title: {
|
|
64
|
+
default: APP_DEFAULT_TITLE,
|
|
65
|
+
template: APP_TITLE_TEMPLATE,
|
|
66
|
+
},
|
|
67
|
+
twitter: {
|
|
68
|
+
card: "summary_large_image",
|
|
69
|
+
description: APP_DESCRIPTION,
|
|
70
|
+
title: {
|
|
71
|
+
default: APP_DEFAULT_TITLE,
|
|
72
|
+
template: APP_TITLE_TEMPLATE,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const viewport: Viewport = {
|
|
78
|
+
colorScheme: "normal",
|
|
79
|
+
themeColor: themes.dark.primary,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export default function Layout({ children }: PropsWithChildren) {
|
|
83
|
+
return (
|
|
84
|
+
<html
|
|
85
|
+
lang="en"
|
|
86
|
+
dir="ltr"
|
|
87
|
+
className={cn(fontsVariable, geist.className)}
|
|
88
|
+
suppressHydrationWarning
|
|
89
|
+
>
|
|
90
|
+
<body>
|
|
91
|
+
<Suspense fallback={null}>
|
|
92
|
+
<SanityStudioGuard>
|
|
93
|
+
{/* Skip link for keyboard navigation accessibility */}
|
|
94
|
+
<Suspense fallback={null}>
|
|
95
|
+
<Link
|
|
96
|
+
href="#main-content"
|
|
97
|
+
className="sr-only focus:not-sr-only focus:fixed focus:top-4 focus:left-4 focus:z-9999 focus:rounded focus:bg-black focus:px-4 focus:py-2 focus:text-white focus:outline-none focus:ring-2 focus:ring-white"
|
|
98
|
+
>
|
|
99
|
+
Skip to main content
|
|
100
|
+
</Link>
|
|
101
|
+
</Suspense>
|
|
102
|
+
</SanityStudioGuard>
|
|
103
|
+
</Suspense>
|
|
104
|
+
|
|
105
|
+
{children}
|
|
106
|
+
|
|
107
|
+
<SanityVisualEditing />
|
|
108
|
+
</body>
|
|
109
|
+
</html>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { MetadataRoute } from "next";
|
|
2
|
+
import { isSanityConfigured } from "@/lib/integrations/check-integration";
|
|
3
|
+
|
|
4
|
+
const APP_BASE_URL =
|
|
5
|
+
process.env.NEXT_PUBLIC_BASE_URL ?? "https://localhost:3000";
|
|
6
|
+
|
|
7
|
+
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|
8
|
+
const baseRoutes: MetadataRoute.Sitemap = [
|
|
9
|
+
{
|
|
10
|
+
url: APP_BASE_URL,
|
|
11
|
+
lastModified: new Date(),
|
|
12
|
+
changeFrequency: "daily",
|
|
13
|
+
priority: 1,
|
|
14
|
+
},
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
// Only fetch Sanity pages if Sanity is configured
|
|
18
|
+
if (isSanityConfigured()) {
|
|
19
|
+
try {
|
|
20
|
+
const sanityModule = await import("@/lib/integrations/sanity/client");
|
|
21
|
+
const sanityGroq = await import("next-sanity");
|
|
22
|
+
|
|
23
|
+
const client = sanityModule?.client;
|
|
24
|
+
const groq = sanityGroq?.groq;
|
|
25
|
+
|
|
26
|
+
// Skip if client is null (shouldn't happen since we check isSanityConfigured)
|
|
27
|
+
if (!(client && groq)) return baseRoutes;
|
|
28
|
+
|
|
29
|
+
type SanityDocument = {
|
|
30
|
+
slug: { current: string };
|
|
31
|
+
_updatedAt: string;
|
|
32
|
+
metadata?: { noIndex?: boolean };
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Fetch all published pages and articles
|
|
36
|
+
const pages = (await client.fetch(
|
|
37
|
+
groq`*[_type == "page" && defined(slug.current)] {
|
|
38
|
+
slug,
|
|
39
|
+
_updatedAt,
|
|
40
|
+
metadata
|
|
41
|
+
}`,
|
|
42
|
+
)) as SanityDocument[];
|
|
43
|
+
|
|
44
|
+
const articles = (await client.fetch(
|
|
45
|
+
groq`*[_type == "article" && defined(slug.current)] {
|
|
46
|
+
slug,
|
|
47
|
+
_updatedAt,
|
|
48
|
+
metadata
|
|
49
|
+
}`,
|
|
50
|
+
)) as SanityDocument[];
|
|
51
|
+
|
|
52
|
+
// Add pages to sitemap (exclude noIndex pages)
|
|
53
|
+
const pageEntries: MetadataRoute.Sitemap = pages
|
|
54
|
+
.filter((page: SanityDocument) => !page.metadata?.noIndex)
|
|
55
|
+
.map((page: SanityDocument) => ({
|
|
56
|
+
url: `${APP_BASE_URL}/${page.slug.current}`,
|
|
57
|
+
lastModified: new Date(page._updatedAt),
|
|
58
|
+
changeFrequency: "weekly" as const,
|
|
59
|
+
priority: 0.8,
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
// Add articles to sitemap (exclude noIndex articles)
|
|
63
|
+
const articleEntries: MetadataRoute.Sitemap = articles
|
|
64
|
+
.filter((article: SanityDocument) => !article.metadata?.noIndex)
|
|
65
|
+
.map((article: SanityDocument) => ({
|
|
66
|
+
url: `${APP_BASE_URL}/blog/${article.slug.current}`,
|
|
67
|
+
lastModified: new Date(article._updatedAt),
|
|
68
|
+
changeFrequency: "weekly" as const,
|
|
69
|
+
priority: 0.7,
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
return [...baseRoutes, ...pageEntries, ...articleEntries];
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error("Error generating sitemap from Sanity:", error);
|
|
75
|
+
return baseRoutes;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return baseRoutes;
|
|
80
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { getImageDimensions } from "@sanity/asset-utils";
|
|
2
|
+
import { Image, type ImageProps } from "@/components/ui/image";
|
|
3
|
+
import { urlForImage } from "@/lib/integrations/sanity/utils/image";
|
|
4
|
+
|
|
5
|
+
interface SanityImageProps extends Omit<ImageProps, "src" | "aspectRatio"> {
|
|
6
|
+
image: {
|
|
7
|
+
asset: {
|
|
8
|
+
_ref: string;
|
|
9
|
+
_type: "reference";
|
|
10
|
+
};
|
|
11
|
+
alt?: string;
|
|
12
|
+
hotspot?: object;
|
|
13
|
+
crop?: object;
|
|
14
|
+
};
|
|
15
|
+
maxWidth?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function SanityImage({
|
|
19
|
+
image,
|
|
20
|
+
maxWidth = 1920,
|
|
21
|
+
alt,
|
|
22
|
+
...props
|
|
23
|
+
}: SanityImageProps) {
|
|
24
|
+
if (!image?.asset) return null;
|
|
25
|
+
|
|
26
|
+
const { width, height } = getImageDimensions(image.asset);
|
|
27
|
+
const aspectRatio = width / height;
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<Image
|
|
31
|
+
src={urlForImage(image).width(maxWidth).url()}
|
|
32
|
+
alt={alt || image.alt || ""}
|
|
33
|
+
aspectRatio={aspectRatio}
|
|
34
|
+
{...props}
|
|
35
|
+
/>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Integrations
|
|
2
|
+
|
|
3
|
+
Third-party service integrations. All are optional—remove unused ones with `bun run setup:project`.
|
|
4
|
+
|
|
5
|
+
## Available Integrations
|
|
6
|
+
|
|
7
|
+
| Integration | Purpose | Documentation |
|
|
8
|
+
|-------------|---------|---------------|
|
|
9
|
+
| [Sanity](sanity/README.md) | Headless CMS | Visual editing, content management |
|
|
10
|
+
| [Shopify](shopify/README.md) | E-commerce | Cart, products, checkout |
|
|
11
|
+
|
|
12
|
+
## Environment Variables
|
|
13
|
+
|
|
14
|
+
```env
|
|
15
|
+
# Sanity CMS
|
|
16
|
+
NEXT_PUBLIC_SANITY_PROJECT_ID="your-project-id"
|
|
17
|
+
NEXT_PUBLIC_SANITY_DATASET="production"
|
|
18
|
+
NEXT_PUBLIC_SANITY_STUDIO_URL="http://localhost:3000/studio"
|
|
19
|
+
SANITY_API_WRITE_TOKEN="your-write-token"
|
|
20
|
+
|
|
21
|
+
# Shopify
|
|
22
|
+
SHOPIFY_STORE_DOMAIN="your-store.myshopify.com"
|
|
23
|
+
SHOPIFY_STOREFRONT_ACCESS_TOKEN="your-token"
|
|
24
|
+
SHOPIFY_REVALIDATION_SECRET="your-secret"
|
|
25
|
+
|
|
26
|
+
# Analytics
|
|
27
|
+
NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID=GTM-XXXXXX
|
|
28
|
+
NEXT_PUBLIC_GOOGLE_ANALYTICS=G-XXXXXXXXXX
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Quick Usage
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
// Sanity
|
|
35
|
+
import { sanityFetch } from '@/integrations/sanity/live'
|
|
36
|
+
import { RichText } from '@/integrations/sanity/components/rich-text'
|
|
37
|
+
const { data } = await sanityFetch({ query: pageQuery })
|
|
38
|
+
|
|
39
|
+
// Shopify
|
|
40
|
+
import { Cart, AddToCart } from '@/lib/integrations/shopify/cart'
|
|
41
|
+
<Cart><AddToCart product={product} /></Cart>
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Removing Integrations
|
|
46
|
+
|
|
47
|
+
Run `bun run setup:project` for interactive removal, or manually:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# Sanity (~150-200KB savings)
|
|
51
|
+
rm -rf lib/integrations/sanity app/studio app/(examples)/sanity
|
|
52
|
+
bun remove @sanity/asset-utils @sanity/image-url next-sanity sanity
|
|
53
|
+
|
|
54
|
+
# Shopify (~50-80KB)
|
|
55
|
+
rm -rf lib/integrations/shopify app/(examples)/shopify
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
After removal: `bun lint:fix && bun build`
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration Configuration Checker
|
|
3
|
+
*
|
|
4
|
+
* Utilities to check if integrations are configured via environment variables.
|
|
5
|
+
* This helps with tree-shaking unused integrations from the bundle.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { isSanityConfigured } from '@/integrations/check-integration'
|
|
10
|
+
*
|
|
11
|
+
* if (isSanityConfigured()) {
|
|
12
|
+
* // Only import and use Sanity code if configured
|
|
13
|
+
* const { sanityFetch } = await import('next-sanity/live')
|
|
14
|
+
* }
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Check if Sanity CMS is configured
|
|
20
|
+
* Requires: NEXT_PUBLIC_SANITY_PROJECT_ID and NEXT_PUBLIC_SANITY_DATASET
|
|
21
|
+
*/
|
|
22
|
+
export function isSanityConfigured(): boolean {
|
|
23
|
+
return Boolean(
|
|
24
|
+
process.env.NEXT_PUBLIC_SANITY_PROJECT_ID &&
|
|
25
|
+
process.env.NEXT_PUBLIC_SANITY_DATASET,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if Google Analytics is configured
|
|
31
|
+
* Requires: NEXT_PUBLIC_GOOGLE_ANALYTICS or NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID
|
|
32
|
+
*/
|
|
33
|
+
export function isAnalyticsConfigured(): boolean {
|
|
34
|
+
return Boolean(
|
|
35
|
+
process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS ||
|
|
36
|
+
process.env.NEXT_PUBLIC_GOOGLE_TAG_MANAGER_ID,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get a list of all configured integrations
|
|
42
|
+
*/
|
|
43
|
+
export function getConfiguredIntegrations(): string[] {
|
|
44
|
+
const integrations: string[] = [];
|
|
45
|
+
|
|
46
|
+
if (isSanityConfigured()) integrations.push("Sanity");
|
|
47
|
+
if (isAnalyticsConfigured()) integrations.push("Analytics");
|
|
48
|
+
|
|
49
|
+
return integrations;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get a list of all unconfigured integrations
|
|
54
|
+
*/
|
|
55
|
+
export function getUnconfiguredIntegrations(): string[] {
|
|
56
|
+
const integrations: string[] = [];
|
|
57
|
+
|
|
58
|
+
if (!isSanityConfigured()) integrations.push("Sanity");
|
|
59
|
+
if (!isAnalyticsConfigured()) integrations.push("Analytics");
|
|
60
|
+
|
|
61
|
+
return integrations;
|
|
62
|
+
}
|