Compare commits
6 Commits
8a35b5ba82
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
a96417508b
|
|||
|
9088eab9df
|
|||
|
|
c32f0a182d | ||
|
102dbbc9ba
|
|||
|
ef1d8dc181
|
|||
|
9d2bd46e72
|
@@ -1,4 +1,4 @@
|
||||
# ✨ Sanity Template
|
||||
# ✨ vaporvee's Sanity Template
|
||||
|
||||
CLI and starter template for building modern web apps with Sveltekit, Sanity, Bun, and Shadcn UI — bundled into a single monorepo.
|
||||
|
||||
|
||||
533
index.js
533
index.js
@@ -1,49 +1,60 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import * as p from '@clack/prompts';
|
||||
import color from 'picocolors';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { spawn, spawnSync } from 'node:child_process';
|
||||
import * as p from "@clack/prompts";
|
||||
import color from "picocolors";
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
async function runCommand(cmd, args, cwd) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const proc = spawn(cmd, args, { cwd, stdio: 'ignore' });
|
||||
proc.on('exit', (code) => (code === 0 ? resolve() : reject(new Error(`Command failed: ${cmd} ${args.join(' ')}`))));
|
||||
const proc = spawn(cmd, args, { cwd, stdio: "ignore" });
|
||||
proc.on("exit", (code) =>
|
||||
code === 0
|
||||
? resolve()
|
||||
: reject(new Error(`Command failed: ${cmd} ${args.join(" ")}`)),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
p.intro(`${color.bgRedBright(color.black(' ✨ Welcome to Lumify Sanity Template! ✨ '))}`);
|
||||
p.intro(
|
||||
`${color.bgRedBright(color.black(" ✨ Welcome to vaporvee's Sanity Template! ✨ "))}`,
|
||||
);
|
||||
|
||||
const project = await p.group(
|
||||
{
|
||||
projectName: () =>
|
||||
p.text({
|
||||
message: `${color.cyan('🔖 Enter your project name (for display):')}`,
|
||||
placeholder: 'My Sanity Lumify page',
|
||||
validate: (v) => (v.length < 2 ? `${color.red('❌ Too short!')}` : undefined),
|
||||
message: `${color.cyan("🔖 Enter your project name (for display):")}`,
|
||||
placeholder: "My Sanity Project",
|
||||
validate: (v) =>
|
||||
v.length < 2 ? `${color.red("❌ Too short!")}` : undefined,
|
||||
}),
|
||||
dirName: ({ results }) => {
|
||||
const defaultKebab = String(results.projectName)
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
return p.text({
|
||||
message: `${color.cyan('📂 Enter a directory name for your project:')}`,
|
||||
message: `${color.cyan("📂 Enter a directory name for your project:")}`,
|
||||
placeholder: `./${defaultKebab}`,
|
||||
initialValue: defaultKebab,
|
||||
validate: (v) => {
|
||||
if (v.length < 2) return `${color.red('❌ Too short!')}`;
|
||||
if (v.length < 2) return `${color.red("❌ Too short!")}`;
|
||||
const kebab = String(v)
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
const root = path.resolve(process.cwd(), kebab);
|
||||
if (fs.existsSync(root)) {
|
||||
return `${color.red('⚠️ Directory already exists:')} ${color.yellow(root)}`;
|
||||
return `${color.red("⚠️ Directory already exists:")} ${color.yellow(root)}`;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
@@ -51,71 +62,73 @@ async function main() {
|
||||
},
|
||||
shouldGit: () =>
|
||||
p.confirm({
|
||||
message: `${color.green('🌱 Do you want to initialize a git repository?')}`,
|
||||
message: `${color.green("🌱 Do you want to initialize a git repository?")}`,
|
||||
initialValue: true,
|
||||
}),
|
||||
packageManager: () =>
|
||||
p.select({
|
||||
message: `${color.magenta('📦 Which package manager do you want to use?')}`,
|
||||
message: `${color.magenta("📦 Which package manager do you want to use?")}`,
|
||||
options: [
|
||||
{ value: 'bun', label: `${color.yellow('bun 🥯')}` },
|
||||
{ value: 'npm', label: `${color.green('npm 📦')}` },
|
||||
{ value: "bun", label: `${color.yellow("bun 🥯")}` },
|
||||
{ value: "npm", label: `${color.green("npm 📦")}` },
|
||||
],
|
||||
initialValue: 'bun',
|
||||
initialValue: "bun",
|
||||
}),
|
||||
lowerCaseName: ({ results }) => {
|
||||
const defaultLowercase = String(results.dirName || results.projectName)
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
return p.text({
|
||||
message: `${color.cyan('📓 Enter a lowercase name for your project (e.g., used as')} ${color.yellow('<input>.sanity.studio')}${color.cyan('):')}`,
|
||||
message: `${color.cyan("📓 Enter a lowercase name for your project (e.g., used as")} ${color.yellow("<input>.sanity.studio")}${color.cyan("):")}`,
|
||||
placeholder: `${defaultLowercase}`,
|
||||
initialValue: defaultLowercase,
|
||||
validate: (v) => {
|
||||
if (v.length < 2) return `${color.red('❌ Too short!')}`;
|
||||
if (!/^[a-z0-9-]+$/.test(v)) return `${color.red('❌ Only lowercase letters, numbers, and hyphens allowed!')}`;
|
||||
if (v.startsWith('-') || v.endsWith('-')) return `${color.red('❌ Cannot start or end with hyphen!')}`;
|
||||
if (v.includes('.sanity.studio')) return `${color.red('❌ Do not include .sanity.studio - it will be added automatically!')}`;
|
||||
if (v.length < 2) return `${color.red("❌ Too short!")}`;
|
||||
if (!/^[a-z0-9-]+$/.test(v))
|
||||
return `${color.red("❌ Only lowercase letters, numbers, and hyphens allowed!")}`;
|
||||
if (v.startsWith("-") || v.endsWith("-"))
|
||||
return `${color.red("❌ Cannot start or end with hyphen!")}`;
|
||||
if (v.includes(".sanity.studio"))
|
||||
return `${color.red("❌ Do not include .sanity.studio - it will be added automatically!")}`;
|
||||
},
|
||||
});
|
||||
},
|
||||
faviconPath: () =>
|
||||
p.text({
|
||||
message: `${color.cyan('🖼️ Path to favicon SVG (leave empty to skip):')}`,
|
||||
placeholder: './myicon.svg',
|
||||
message: `${color.cyan("🖼️ Path to favicon SVG (leave empty to skip):")}`,
|
||||
placeholder: "./myicon.svg",
|
||||
}),
|
||||
extraDomain: () =>
|
||||
p.text({
|
||||
message: `${color.cyan('🌍 Enter a domain this will run on later (leave empty to skip):')}`,
|
||||
placeholder: 'youronlinedomain.com',
|
||||
message: `${color.cyan("🌍 Enter a domain this will run on later (leave empty to skip):")}`,
|
||||
placeholder: "youronlinedomain.com",
|
||||
validate: () => undefined,
|
||||
}),
|
||||
},
|
||||
{
|
||||
onCancel: () => {
|
||||
p.cancel('Operation cancelled.');
|
||||
p.cancel("Operation cancelled.");
|
||||
process.exit(0);
|
||||
},
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const kebabName = String(project.dirName)
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
const rootDir = path.join(process.cwd(), kebabName);
|
||||
const pmx = project.packageManager === 'bun' ? 'bun x' : 'npx';
|
||||
const pm = project.packageManager === 'bun' ? 'bun' : 'npm';
|
||||
const studioDir = path.join(rootDir, 'apps', 'studio');
|
||||
const pm = project.packageManager === "bun" ? "bun" : "npm";
|
||||
const studioDir = path.join(rootDir, "apps", "studio");
|
||||
|
||||
await p.tasks([
|
||||
{
|
||||
title: `${color.yellow('📁 Copying template contents to root')}`,
|
||||
title: `${color.yellow("📁 Copying template contents to root")}`,
|
||||
task: async () => {
|
||||
const templateDir = path.join(process.cwd(), 'template');
|
||||
const templateDir = path.join(__dirname, "template");
|
||||
|
||||
// Ensure root directory exists with proper permissions
|
||||
await fs.ensureDir(rootDir);
|
||||
@@ -124,7 +137,7 @@ async function main() {
|
||||
await fs.copy(templateDir, rootDir, {
|
||||
overwrite: true,
|
||||
errorOnExist: false,
|
||||
preserveTimestamps: false
|
||||
preserveTimestamps: false,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to copy template files: ${error.message}`);
|
||||
@@ -140,160 +153,268 @@ async function main() {
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
await renameGitignoreFiles(itemPath);
|
||||
} else if (item === 'gitignore') {
|
||||
const gitignorePath = path.join(dir, '.gitignore');
|
||||
} else if (item === "gitignore") {
|
||||
const gitignorePath = path.join(dir, ".gitignore");
|
||||
await fs.move(itemPath, gitignorePath);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to rename gitignore files: ${error.message}`);
|
||||
throw new Error(
|
||||
`Failed to rename gitignore files: ${error.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await renameGitignoreFiles(rootDir);
|
||||
return 'Template copied!';
|
||||
return "Template copied!";
|
||||
},
|
||||
},
|
||||
{
|
||||
title: `${color.green(`📦 Installing dependencies with ${project.packageManager}`)}`,
|
||||
task: async () => {
|
||||
await runCommand(project.packageManager === 'bun' ? 'bun' : 'npm', ['install'], rootDir);
|
||||
return 'Dependencies installed successfully';
|
||||
await runCommand(
|
||||
project.packageManager === "bun" ? "bun" : "npm",
|
||||
["install"],
|
||||
rootDir,
|
||||
);
|
||||
return "Dependencies installed successfully";
|
||||
},
|
||||
},
|
||||
{
|
||||
title: `${color.cyan('🔑 Checking Sanity login status')}`,
|
||||
task: async () => {
|
||||
await fs.ensureDir(studioDir);
|
||||
let loggedIn = false;
|
||||
let hasProjects = false;
|
||||
try {
|
||||
const result = await new Promise((res, rej) => {
|
||||
let out = '';
|
||||
const c = spawn(pmx, ['sanity', 'projects', 'list'], { cwd: studioDir, stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
c.stdout.on('data', (d) => (out += d));
|
||||
c.on('exit', (code) => (code === 0 ? res(out) : rej(new Error('fail'))));
|
||||
});
|
||||
if (result.includes('id ')) {
|
||||
loggedIn = true;
|
||||
hasProjects = result.split('\n').slice(1).some((l) => l.trim());
|
||||
}
|
||||
} catch { }
|
||||
if (loggedIn) {
|
||||
return hasProjects ? 'Already logged in to Sanity!' : 'Logged in to Sanity (no projects yet).';
|
||||
} else {
|
||||
await runCommand(pmx, ['sanity', 'login'], studioDir);
|
||||
return 'Sanity login complete!';
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: `${color.magenta('🛠️ Creating Sanity project')}`,
|
||||
task: async () => {
|
||||
spawnSync(pmx, [
|
||||
"sanity",
|
||||
"projects",
|
||||
"create",
|
||||
project.projectName
|
||||
], {
|
||||
cwd: studioDir,
|
||||
stdio: "inherit",
|
||||
encoding: "utf-8"
|
||||
]);
|
||||
|
||||
// Now check Sanity login status and prompt for provider if needed
|
||||
console.log(`${color.cyan("🔑 Checking Sanity login status...")}`);
|
||||
let loggedIn = false;
|
||||
let hasProjects = false;
|
||||
|
||||
try {
|
||||
const result = await new Promise((res, rej) => {
|
||||
let out = "";
|
||||
const c = spawn(pm, ["x", "sanity", "projects", "list"], {
|
||||
cwd: studioDir,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
c.stdout.on("data", (d) => (out += d));
|
||||
c.on("exit", (code) =>
|
||||
code === 0 ? res(out) : rej(new Error("Not logged in")),
|
||||
);
|
||||
});
|
||||
if (result.includes("id ")) {
|
||||
loggedIn = true;
|
||||
hasProjects = result
|
||||
.split("\n")
|
||||
.slice(1)
|
||||
.some((l) => l.trim());
|
||||
}
|
||||
} catch {}
|
||||
|
||||
if (!loggedIn) {
|
||||
// Exit clack UI completely for interactive commands
|
||||
p.outro(`${color.yellow("⚠️ Authentication required...")}`);
|
||||
|
||||
// Clear terminal and use clean console session
|
||||
console.clear();
|
||||
console.log(
|
||||
`\n${color.bgRedBright(color.black(" ✨ vaporvee's Sanity Template - Authentication ✨ "))}\n`,
|
||||
);
|
||||
|
||||
if (!loggedIn) {
|
||||
p.log.warn(`${color.yellow("⚠️ You need to login to Sanity first.")}`);
|
||||
const provider = await p.select({
|
||||
message: `${color.cyan("🔐 Choose a provider to login to Sanity:")}`,
|
||||
options: [
|
||||
{ value: "sanity", label: "Sanity (email/password)" },
|
||||
{ value: "google", label: "Google" },
|
||||
{ value: "github", label: "GitHub" },
|
||||
],
|
||||
initialValue: "sanity",
|
||||
});
|
||||
|
||||
p.log.info(`${color.cyan(`🚀 Logging in with provider: ${provider}`)}`);
|
||||
|
||||
try {
|
||||
await runCommand(
|
||||
pm,
|
||||
["x", "sanity", "login", "--provider", provider],
|
||||
studioDir,
|
||||
);
|
||||
p.log.success(`${color.green("✅ Sanity login complete!")}`);
|
||||
} catch (error) {
|
||||
p.log.error(`${color.red("❌ Sanity login failed:")}`);
|
||||
p.log.error(error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
p.log.success(
|
||||
hasProjects
|
||||
? `${color.green("✅ Already logged in to Sanity!")}`
|
||||
: `${color.green("✅ Logged in to Sanity (no projects yet).")}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear terminal for clean project creation
|
||||
// Create Sanity project (manual step to avoid interaction issues)
|
||||
p.log.info(`${color.magenta("🛠️ Creating Sanity project...")}`);
|
||||
p.log.warn(
|
||||
`${color.yellow("⚠️ Due to terminal interaction issues, you need to run this command manually:")}`,
|
||||
);
|
||||
p.log.message(`${color.cyan("cd " + studioDir)}`);
|
||||
p.log.message(
|
||||
`${color.cyan(`${pm} x sanity projects create "${project.projectName}"`)}`,
|
||||
);
|
||||
|
||||
await p.confirm({
|
||||
message: `${color.green("Press Enter when the project is created successfully...")}`,
|
||||
initialValue: true,
|
||||
});
|
||||
|
||||
p.log.success(
|
||||
`${color.green("✅ Continuing with project configuration...")}`,
|
||||
);
|
||||
|
||||
const listProjects = spawnSync(pm, ["x", "sanity", "projects", "list"], {
|
||||
cwd: studioDir,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
encoding: "utf-8",
|
||||
});
|
||||
|
||||
let sanityJson;
|
||||
if (listProjects.stdout) {
|
||||
try {
|
||||
const lines = listProjects.stdout
|
||||
.split(/\r?\n/)
|
||||
.filter((l) => l.trim() && !l.startsWith("id") && !l.startsWith("─"));
|
||||
const projectIds = lines
|
||||
.map((line) => {
|
||||
const match = line.match(/^([a-z0-9]{8})\b/);
|
||||
return match ? match[1] : null;
|
||||
})
|
||||
.filter((id) => !!id);
|
||||
|
||||
const listProjects = spawnSync(pmx, [
|
||||
"sanity",
|
||||
"projects",
|
||||
"list"
|
||||
], {
|
||||
cwd: studioDir,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
encoding: "utf-8"
|
||||
})
|
||||
if (projectIds.length > 0) {
|
||||
sanityJson = { projectId: projectIds[0] };
|
||||
}
|
||||
} catch (e) {
|
||||
p.log.error(
|
||||
`${color.red("❌ Could not parse Sanity project list output.")}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
if (!sanityJson || !sanityJson.projectId) {
|
||||
console.error(`${color.red("❌ Sanity project creation failed.")}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let sanityJson;
|
||||
if (listProjects.stdout) {
|
||||
try {
|
||||
const lines = listProjects.stdout.split(/\r?\n/).filter(l => l.trim() && !l.startsWith('id') && !l.startsWith('─'));
|
||||
const projectIds = lines.map(line => {
|
||||
const match = line.match(/^([a-z0-9]{8})\b/);
|
||||
return match ? match[1] : null;
|
||||
}).filter(id => !!id);
|
||||
// Update connection file with project ID
|
||||
const connPath = path.join(rootDir, "packages/sanity-connection/index.ts");
|
||||
if (!(await fs.pathExists(connPath))) {
|
||||
console.error(
|
||||
`${color.red("❌ Connection file not found at " + connPath)}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
let connFile = await fs.readFile(connPath, "utf8");
|
||||
if (!/projectId: "[^"]+"/.test(connFile)) {
|
||||
console.error(
|
||||
`${color.red("❌ Could not find projectId in connection file")}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
connFile = connFile.replace(
|
||||
/projectId: "[^"]+"/,
|
||||
`projectId: "${sanityJson.projectId}"`,
|
||||
);
|
||||
await fs.writeFile(connPath, connFile);
|
||||
|
||||
if (projectIds.length > 0) {
|
||||
sanityJson = { projectId: projectIds[0] };
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error("Could not parse Sanity project list output.");
|
||||
}
|
||||
}
|
||||
if (!sanityJson || !sanityJson.projectId) {
|
||||
throw new Error("Sanity project creation failed.");
|
||||
}
|
||||
// Update wrangler.jsonc with project name
|
||||
const wranglerPath = path.join(rootDir, "apps/client/wrangler.jsonc");
|
||||
if (await fs.pathExists(wranglerPath)) {
|
||||
let wranglerFile = await fs.readFile(wranglerPath, "utf8");
|
||||
wranglerFile = wranglerFile.replace(
|
||||
/"name": "[^"]+"/,
|
||||
`"name": "${project.lowerCaseName}"`,
|
||||
);
|
||||
await fs.writeFile(wranglerPath, wranglerFile);
|
||||
}
|
||||
|
||||
// Update connection file with project ID
|
||||
const connPath = path.join(rootDir, "packages/sanity-connection/index.ts");
|
||||
if (!(await fs.pathExists(connPath))) {
|
||||
throw new Error(`Connection file not found at ${connPath}`);
|
||||
}
|
||||
let connFile = await fs.readFile(connPath, "utf8");
|
||||
if (!/projectId: "[^"]+"/.test(connFile)) {
|
||||
throw new Error(`Could not find projectId in ${connPath}. Please ensure the file contains a line like projectId: "${sanityJson.projectId}"`);
|
||||
}
|
||||
connFile = connFile.replace(/projectId: "[^"]+"/, `projectId: "${sanityJson.projectId}"`);
|
||||
await fs.writeFile(connPath, connFile);
|
||||
p.log.success(
|
||||
`${color.green("✅ Sanity project created with ID: " + sanityJson.projectId)}`,
|
||||
);
|
||||
|
||||
// Update wrangler.jsonc with project name
|
||||
const wranglerPath = path.join(rootDir, "apps/client/wrangler.jsonc");
|
||||
if (await fs.pathExists(wranglerPath)) {
|
||||
let wranglerFile = await fs.readFile(wranglerPath, "utf8");
|
||||
wranglerFile = wranglerFile.replace(/"name": "[^"]+"/, `"name": "${project.lowerCaseName}"`);
|
||||
await fs.writeFile(wranglerPath, wranglerFile);
|
||||
}
|
||||
|
||||
return `Sanity project created with ID: ${sanityJson.projectId}`;
|
||||
},
|
||||
},
|
||||
await p.tasks([
|
||||
{
|
||||
title: `${color.blue('🗄️ Setting up production dataset for Sanity CMS')}`,
|
||||
title: `${color.blue("🗄️ Setting up production dataset for Sanity CMS")}`,
|
||||
task: async () => {
|
||||
if (!(await fs.pathExists(studioDir))) throw new Error(`Studio directory not found at ${studioDir}`);
|
||||
if (!(await fs.pathExists(studioDir)))
|
||||
throw new Error(`Studio directory not found at ${studioDir}`);
|
||||
|
||||
try {
|
||||
await runCommand(pmx, ['sanity', 'dataset', 'create', 'production'], studioDir);
|
||||
return 'Production dataset created successfully';
|
||||
await runCommand(
|
||||
pm,
|
||||
["x", "sanity", "dataset", "create", "production"],
|
||||
studioDir,
|
||||
);
|
||||
return "Production dataset created successfully";
|
||||
} catch (error) {
|
||||
// Check if dataset already exists
|
||||
if (error.message && error.message.includes('already exists')) {
|
||||
return 'Production dataset already exists';
|
||||
if (error.message && error.message.includes("already exists")) {
|
||||
return "Production dataset already exists";
|
||||
}
|
||||
throw new Error(`Failed to create production dataset: ${error.message}`);
|
||||
throw new Error(
|
||||
`Failed to create production dataset: ${error.message}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: `${color.green('🔐 Creating Sanity viewer token')}`,
|
||||
title: `${color.green("🔐 Creating Sanity viewer token")}`,
|
||||
task: async () => {
|
||||
if (!(await fs.pathExists(studioDir))) throw new Error(`Studio directory not found at ${studioDir}`);
|
||||
if (!(await fs.pathExists(studioDir)))
|
||||
throw new Error(`Studio directory not found at ${studioDir}`);
|
||||
const addOut = await new Promise((res, rej) => {
|
||||
let buf = '';
|
||||
let buf = "";
|
||||
const c = spawn(
|
||||
pmx,
|
||||
['sanity', 'tokens', 'add', 'Main Viewer API Token', '--role=viewer', '-y', '--json'],
|
||||
{ cwd: studioDir, stdio: ['ignore', 'pipe', 'pipe'] }
|
||||
pm,
|
||||
[
|
||||
"x",
|
||||
"sanity",
|
||||
"tokens",
|
||||
"add",
|
||||
"Main Viewer API Token",
|
||||
"--role=viewer",
|
||||
"-y",
|
||||
"--json",
|
||||
],
|
||||
{ cwd: studioDir, stdio: ["ignore", "pipe", "pipe"] },
|
||||
);
|
||||
c.stdout.on("data", (d) => (buf += d));
|
||||
c.on("exit", (code) =>
|
||||
code === 0 ? res(buf) : rej(new Error("token failed")),
|
||||
);
|
||||
c.stdout.on('data', (d) => (buf += d));
|
||||
c.on('exit', (code) => (code === 0 ? res(buf) : rej(new Error('token failed'))));
|
||||
});
|
||||
const token = JSON.parse(addOut);
|
||||
const connPath = path.join(rootDir, 'packages/sanity-connection/index.ts');
|
||||
let content = await fs.readFile(connPath, 'utf8');
|
||||
const connPath = path.join(
|
||||
rootDir,
|
||||
"packages/sanity-connection/index.ts",
|
||||
);
|
||||
let content = await fs.readFile(connPath, "utf8");
|
||||
content = content
|
||||
.replace(/publicViewerToken: "[^"]+"/, `publicViewerToken: "${token.key}"`)
|
||||
.replace(/studioHost: "[^"]+"/, `studioHost: "${project.lowerCaseName}"`)
|
||||
.replace(/studioUrl: "[^"]+"/, `studioUrl: "https://${project.lowerCaseName}.sanity.studio"`);
|
||||
.replace(
|
||||
/publicViewerToken: "[^"]+"/,
|
||||
`publicViewerToken: "${token.key}"`,
|
||||
)
|
||||
.replace(
|
||||
/studioHost: "[^"]+"/,
|
||||
`studioHost: "${project.lowerCaseName}"`,
|
||||
)
|
||||
.replace(
|
||||
/studioUrl: "[^"]+"/,
|
||||
`studioUrl: "https://${project.lowerCaseName}.sanity.studio"`,
|
||||
);
|
||||
await fs.writeFile(connPath, content);
|
||||
return 'Viewer token created and configured!';
|
||||
return "Viewer token created and configured!";
|
||||
},
|
||||
},
|
||||
]);
|
||||
@@ -301,21 +422,33 @@ async function main() {
|
||||
if (project.faviconPath && project.faviconPath.trim()) {
|
||||
await p.tasks([
|
||||
{
|
||||
title: `${color.yellow('🌟 Generating favicon')}`,
|
||||
title: `${color.yellow("🌟 Generating favicon")}`,
|
||||
task: async () => {
|
||||
await runCommand(pm, ['create', "favicon", project.faviconPath, 'packages/ui/favicon'], rootDir);
|
||||
await fs.copy(path.join(rootDir, 'packages/ui/favicon/'), path.join(rootDir, 'apps/client/public/'), { overwrite: true });
|
||||
await fs.copy(path.join(rootDir, 'packages/ui/favicon/'), path.join(rootDir, 'apps/studio/static/'), { overwrite: true });
|
||||
return 'Favicon generated and copied!';
|
||||
await runCommand(
|
||||
pm,
|
||||
["create", "favicon", project.faviconPath, "packages/ui/favicon"],
|
||||
rootDir,
|
||||
);
|
||||
await fs.copy(
|
||||
path.join(rootDir, "packages/ui/favicon/"),
|
||||
path.join(rootDir, "apps/client/public/"),
|
||||
{ overwrite: true },
|
||||
);
|
||||
await fs.copy(
|
||||
path.join(rootDir, "packages/ui/favicon/"),
|
||||
path.join(rootDir, "apps/studio/static/"),
|
||||
{ overwrite: true },
|
||||
);
|
||||
return "Favicon generated and copied!";
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
const corsOrigins = [
|
||||
'http://localhost:5173',
|
||||
'https://*.api.sanity.io',
|
||||
'wss://*.api.sanity.io',
|
||||
"http://localhost:5173",
|
||||
"https://*.api.sanity.io",
|
||||
"wss://*.api.sanity.io",
|
||||
`https://${project.lowerCaseName}.sanity.studio`,
|
||||
];
|
||||
if (project.extraDomain && project.extraDomain.trim()) {
|
||||
@@ -323,27 +456,43 @@ async function main() {
|
||||
}
|
||||
await p.tasks(
|
||||
corsOrigins.map((origin) => ({
|
||||
title: `${color.cyan('🌐 Adding Sanity CORS origin:')} ${color.yellow(origin)}`,
|
||||
title: `${color.cyan("🌐 Adding Sanity CORS origin:")} ${color.yellow(origin)}`,
|
||||
task: async () => {
|
||||
const args = ['sanity', 'cors', 'add', origin, '--yes'];
|
||||
if (origin === `https://${project.lowerCaseName}.sanity.studio`) args.push('--credentials');
|
||||
await runCommand(pmx, args, studioDir);
|
||||
return `CORS added: ${origin}` + (args.includes('--credentials') ? ' (credentials allowed)' : '');
|
||||
const args = ["x", "sanity", "cors", "add", origin, "--yes"];
|
||||
if (origin === `https://${project.lowerCaseName}.sanity.studio`)
|
||||
args.push("--credentials");
|
||||
await runCommand(pm, args, studioDir);
|
||||
return (
|
||||
`CORS added: ${origin}` +
|
||||
(args.includes("--credentials") ? " (credentials allowed)" : "")
|
||||
);
|
||||
},
|
||||
}))
|
||||
})),
|
||||
);
|
||||
|
||||
if (project.shouldGit) {
|
||||
await p.tasks([
|
||||
{
|
||||
title: `${color.green('🌱 Setting up Git repository')}`,
|
||||
title: `${color.green("🌱 Setting up Git repository")}`,
|
||||
task: async () => {
|
||||
await runCommand('git', ['config', '--global', 'init.defaultBranch', 'main'], process.cwd());
|
||||
await runCommand('git', ['init'], rootDir);
|
||||
await runCommand('git', ['add', '.'], rootDir);
|
||||
await runCommand('git', ['commit', '-m', 'Initiated from Lumify Sanity template :rocket:'], rootDir);
|
||||
await runCommand('git', ['branch', '-M', 'main'], rootDir);
|
||||
return 'Git repository initialized with main branch';
|
||||
await runCommand(
|
||||
"git",
|
||||
["config", "--global", "init.defaultBranch", "main"],
|
||||
process.cwd(),
|
||||
);
|
||||
await runCommand("git", ["init"], rootDir);
|
||||
await runCommand("git", ["add", "."], rootDir);
|
||||
await runCommand(
|
||||
"git",
|
||||
[
|
||||
"commit",
|
||||
"-m",
|
||||
"Initiated from vaporvee's Sanity template :rocket:",
|
||||
],
|
||||
rootDir,
|
||||
);
|
||||
await runCommand("git", ["branch", "-M", "main"], rootDir);
|
||||
return "Git repository initialized with main branch";
|
||||
},
|
||||
},
|
||||
]);
|
||||
@@ -353,27 +502,27 @@ async function main() {
|
||||
const pmDeploy = `${project.packageManager} run deploy`;
|
||||
|
||||
p.text({
|
||||
message:
|
||||
`${color.bgGreen(color.black('✨ Setup complete! ✨'))}\n${color.dim('Need help?')} ${color.underline(color.cyan('https://duckduckgo.com/'))} ${color.yellow('💡')}`,
|
||||
message: `${color.bgGreen(color.black("✨ Setup complete! ✨"))}\n${color.dim("Need help?")} ${color.underline(color.cyan("https://duckduckgo.com/"))} ${color.yellow("💡")}`,
|
||||
});
|
||||
|
||||
p.outro(
|
||||
color.bold(color.cyan('🎉 All done! Your project is ready! 🎉')) + '\n\n' +
|
||||
color.bold('Next steps:\n') +
|
||||
` ${color.bold(color.green('cd'))} ${color.cyan(kebabName)}\n` +
|
||||
` ${color.bold(color.green(pmRun))} ${color.dim('# Start your local dev server')} ${color.yellow('🖥️')}\n` +
|
||||
` ${color.bold(color.green(pmDeploy))} ${color.dim('# Deploy your Sanity Studio')} ${color.yellow('🚀')}\n\n` +
|
||||
color.dim('Local development:\n') +
|
||||
` • App: ${color.cyan('http://localhost:5173')} ${color.yellow('🌐')}\n` +
|
||||
` • Studio: ${color.cyan('http://localhost:3333')} ${color.yellow('🛠️')}\n\n` +
|
||||
color.dim('After deploying:\n') +
|
||||
` • Studio: ${color.cyan(`https://${project.lowerCaseName}.sanity.studio`)} ${color.yellow('✨')}`
|
||||
color.bold(color.cyan("🎉 All done! Your project is ready! 🎉")) +
|
||||
"\n\n" +
|
||||
color.bold("Next steps:\n") +
|
||||
` ${color.bold(color.green("cd"))} ${color.cyan(kebabName)}\n` +
|
||||
` ${color.bold(color.green(pmRun))} ${color.dim("# Start your local dev server")} ${color.yellow("🖥️")}\n` +
|
||||
` ${color.bold(color.green(pmDeploy))} ${color.dim("# Deploy your Sanity Studio")} ${color.yellow("🚀")}\n\n` +
|
||||
color.dim("Local development:\n") +
|
||||
` • App: ${color.cyan("http://localhost:5173")} ${color.yellow("🌐")}\n` +
|
||||
` • Studio: ${color.cyan("http://localhost:3333")} ${color.yellow("🛠️")}\n\n` +
|
||||
color.dim("After deploying:\n") +
|
||||
` • Studio: ${color.cyan(`https://${project.lowerCaseName}.sanity.studio`)} ${color.yellow("✨")}`,
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
p.log.error('An error occurred during setup:');
|
||||
p.log.error("An error occurred during setup:");
|
||||
p.log.error(err.message || err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
12
package.json
12
package.json
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "@lumify-systems/template-sanity",
|
||||
"version": "2.1.2",
|
||||
"name": "@vaporvee/template-sanity",
|
||||
"version": "2.2.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted",
|
||||
"registry": "https://npm.pkg.github.com"
|
||||
"registry": "https://git.vaporvee.com/api/packages/vaporvee/npm"
|
||||
},
|
||||
"description": "Template for Lumify Sanity and Svelte monorepo setup.",
|
||||
"description": "Template for Sanity and Svelte monorepo setup.",
|
||||
"bin": {
|
||||
"template-sanity": "./index.js"
|
||||
},
|
||||
@@ -14,8 +14,8 @@
|
||||
"bun",
|
||||
"create",
|
||||
"template",
|
||||
"lumify",
|
||||
"sanity"
|
||||
"sanity",
|
||||
"svelte"
|
||||
],
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
# Navbar Integration Complete ✅
|
||||
|
||||
This document summarizes the completed integration between the Sanity navbar schema and the SvelteKit frontend component.
|
||||
|
||||
## What Was Implemented
|
||||
|
||||
### 1. Navigation Data Service (`src/lib/navigation.ts`)
|
||||
- ✅ Fetches navbar documents from Sanity
|
||||
- ✅ Processes different sublink types (auto, tag, manual)
|
||||
- ✅ Resolves internal links using the link helper
|
||||
- ✅ Implements 5-minute caching for performance
|
||||
- ✅ Provides fallback navigation when no document exists
|
||||
- ✅ Uses generated TypeScript types for type safety
|
||||
|
||||
### 2. Updated Layout Integration
|
||||
- ✅ Modified `+layout.server.ts` to load navigation data
|
||||
- ✅ Updated `+layout.svelte` to pass navigation to navbar component
|
||||
- ✅ Added proper TypeScript return types
|
||||
- ✅ Server-side rendering for better SEO and performance
|
||||
|
||||
### 3. Enhanced Navbar Component
|
||||
- ✅ Removed hardcoded navigation items
|
||||
- ✅ Uses dynamic data from Sanity
|
||||
- ✅ Handles empty/undefined navigation gracefully
|
||||
- ✅ Maintains all existing UI functionality (mobile menu, animations, etc.)
|
||||
- ✅ Supports nested submenus from Sanity schema
|
||||
|
||||
### 4. Link Processing Integration
|
||||
- ✅ Uses existing `deconstructLink` helper for all link types
|
||||
- ✅ Supports static, external, email, phone, and internal links
|
||||
- ✅ Resolves internal page references to proper URLs
|
||||
- ✅ Handles different page types (custom, blog) correctly
|
||||
|
||||
## Schema Features Implemented
|
||||
|
||||
### Main Navigation Links
|
||||
- ✅ Text and link fields
|
||||
- ✅ All link types supported via link helper
|
||||
- ✅ Optional sublinks array
|
||||
|
||||
### Auto Sublinks
|
||||
- ✅ Fetches 5 most recent pages by type (custom/blog)
|
||||
- ✅ Automatic URL generation based on page type
|
||||
- ✅ Proper GROQ query with ordering and limits
|
||||
|
||||
### Tag-based Sublinks
|
||||
- ✅ Filters pages by selected tag reference
|
||||
- ✅ Configurable page type for tag filtering
|
||||
- ✅ Automatic page fetching and URL generation
|
||||
|
||||
### Manual Sublinks
|
||||
- ✅ Custom text and link configuration
|
||||
- ✅ Full link helper integration
|
||||
- ✅ Fallback handling for invalid links
|
||||
|
||||
## Performance Features
|
||||
|
||||
### Caching System
|
||||
- ✅ 5-minute in-memory cache
|
||||
- ✅ Reduces database queries on each page load
|
||||
- ✅ Automatic cache invalidation
|
||||
- ✅ Development-friendly cache clearing
|
||||
|
||||
### Server-Side Processing
|
||||
- ✅ All link resolution happens server-side
|
||||
- ✅ Better SEO with pre-rendered navigation
|
||||
- ✅ Faster client-side hydration
|
||||
- ✅ Reduced client-side JavaScript
|
||||
|
||||
## Developer Experience
|
||||
|
||||
### Type Safety
|
||||
- ✅ Generated TypeScript types from Sanity schema
|
||||
- ✅ Proper interface definitions for navigation items
|
||||
- ✅ Type-safe data flow from Sanity to component
|
||||
- ✅ IntelliSense support throughout the stack
|
||||
|
||||
### Error Handling
|
||||
- ✅ Graceful degradation when Sanity is unavailable
|
||||
- ✅ Console logging for debugging
|
||||
- ✅ Fallback navigation for empty documents
|
||||
- ✅ Safe handling of missing or invalid data
|
||||
|
||||
### Setup Tools
|
||||
- ✅ Automated setup script (`scripts/setup-navbar.js`)
|
||||
- ✅ Package.json script integration
|
||||
- ✅ Comprehensive documentation
|
||||
- ✅ Environment variable configuration
|
||||
|
||||
## File Changes Summary
|
||||
|
||||
### New Files Created
|
||||
- `src/lib/navigation.ts` - Navigation data service
|
||||
- `scripts/setup-navbar.js` - Initial document creation script
|
||||
- `NAVBAR_INTEGRATION.md` - Technical documentation
|
||||
- `NAVBAR_SETUP.md` - User setup guide
|
||||
- `INTEGRATION_COMPLETE.md` - This summary
|
||||
|
||||
### Modified Files
|
||||
- `+layout.server.ts` - Added navigation data loading
|
||||
- `+layout.svelte` - Pass navigation to navbar component
|
||||
- `navbar.svelte` - Use dynamic data instead of hardcoded
|
||||
- `package.json` - Added setup script and dependencies
|
||||
|
||||
### Generated Files
|
||||
- `sanity.types.ts` - Updated with Navbar type definitions
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Setup
|
||||
```bash
|
||||
# Generate types
|
||||
cd apps/studio && bun run generate
|
||||
|
||||
# Create navbar document
|
||||
SANITY_PROJECT_ID=your-id SANITY_TOKEN=your-token bun run setup:navbar
|
||||
```
|
||||
|
||||
### Component Usage
|
||||
```svelte
|
||||
<!-- Navigation is automatically loaded an
|
||||
@@ -1,98 +0,0 @@
|
||||
# SvelteKit Routing Guide
|
||||
|
||||
This guide explains how the dynamic routing system works in this Sanity + SvelteKit template, including URL patterns and content management.
|
||||
|
||||
## URL Structure
|
||||
|
||||
The template uses the following URL patterns:
|
||||
|
||||
### Custom Pages
|
||||
- **Pattern**: `/{slug}`
|
||||
- **Example**: `/about`, `/contact`, `/services`
|
||||
- **Content Type**: `custom` pages from Sanity
|
||||
- **Route File**: `src/routes/[slug]/+page.svelte`
|
||||
|
||||
### Blog Posts
|
||||
- **Pattern**: `/blog/{slug}`
|
||||
- **Example**: `/blog/my-first-post`, `/blog/web-development-tips`
|
||||
- **Content Type**: `blog` posts from Sanity
|
||||
- **Route File**: `src/routes/blog/[slug]/+page.svelte`
|
||||
|
||||
### Blog Index
|
||||
- **Pattern**: `/blog`
|
||||
- **Shows**: List of all blog posts
|
||||
- **Route File**: `src/routes/blog/+page.svelte`
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
src/routes/
|
||||
├── [slug]/ # Custom pages at root level
|
||||
│ ├── +page.server.ts # Server-side data loading
|
||||
│ └── +page.svelte # Custom page component
|
||||
├── blog/
|
||||
│ ├── +page.server.ts # Blog index server load
|
||||
│ ├── +page.svelte # Blog index component
|
||||
│ └── [slug]/ # Individual blog posts
|
||||
│ ├── +page.server.ts # Blog post server load
|
||||
│ └── +page.svelte # Blog post component
|
||||
├── +layout.server.ts # Global layout data
|
||||
└── +layout.svelte # Global layout component
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
### 1. URL Resolution Order
|
||||
|
||||
SvelteKit resolves URLs in this order:
|
||||
1. **Exact routes** (e.g., `/blog`)
|
||||
2. **Parameterized routes** (e.g., `/blog/[slug]`, `/[slug]`)
|
||||
|
||||
This means:
|
||||
- `/blog` → Blog index page
|
||||
- `/blog/some-post` → Blog post page
|
||||
- `/about` → Custom page (if exists)
|
||||
- `/nonexistent` → 404 error
|
||||
|
||||
### 2. Data Loading Flow
|
||||
|
||||
#### Custom Pages (`/[slug]`)
|
||||
|
||||
```typescript
|
||||
// +page.server.ts
|
||||
const CUSTOM_QUERY = `*[_type == "custom" && slug.current == $slug][0]`;
|
||||
const custom = await serverClient.fetch(CUSTOM_QUERY, { slug });
|
||||
```
|
||||
|
||||
#### Blog Posts (`/blog/[slug]`)
|
||||
|
||||
```typescript
|
||||
// +page.server.ts
|
||||
const BLOG_QUERY = `*[_type == "blog" && slug.current == $slug][0]`;
|
||||
const blog = await serverClient.fetch(BLOG_QUERY, { slug });
|
||||
```
|
||||
|
||||
### 3. Content Types
|
||||
|
||||
#### Custom Page Schema
|
||||
```typescript
|
||||
type Custom = {
|
||||
_id: string;
|
||||
_type: 'custom';
|
||||
title?: string;
|
||||
slug?: Slug;
|
||||
tags?: Array<string>;
|
||||
publishedAt?: string;
|
||||
body?: BlockContent;
|
||||
seoTitle?: string;
|
||||
seoDescription?: string;
|
||||
}
|
||||
```
|
||||
|
||||
#### Blog Post Schema
|
||||
```typescript
|
||||
type Blog = {
|
||||
_id: string;
|
||||
_type: 'blog';
|
||||
title?: string;
|
||||
slug
|
||||
@@ -1,5 +1,5 @@
|
||||
export const sanityConnection = {
|
||||
pageTitle: "lumify template",
|
||||
pageTitle: "sanity template",
|
||||
publicViewerToken: "skC2bGanXZGHQoerK2vIkyfo7Bl9dgYgCrHSHNPnnsO81e64HLLtExpg84NaUkkWTtd1oJ4QBqa5qn6MrezYAEPYXjgfV6MrtFr1LqEz1BKQaO3uZHHJlSfwHndTveDUWp4MbgeYj7CTsPlN7SWDVwP6P6JLodSYmEyoTN7eiE06e2FlogbM",
|
||||
studioHost: "vaporvee-test",
|
||||
studioUrl: "https://vaporvee-test.sanity.studio", // normaly https://<studioHost>.sanity.studio
|
||||
|
||||
Reference in New Issue
Block a user