diff --git a/.gitignore b/.gitignore
index 3f4c2a4..90ce5f4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,7 +31,11 @@ yarn-error.log*
*.pem
# NX
-.nx/cache
-.nx/workspace-data
-.cursor/rules/nx-rules.mdc
-.github/instructions/nx.instructions.md
\ No newline at end of file
+.nx
+
+# SvelteKit
+.svelte-kit
+.wrangler
+
+# Sanity
+.sanity
\ No newline at end of file
diff --git a/index.js b/index.js
index 7927def..10e0920 100755
--- a/index.js
+++ b/index.js
@@ -72,6 +72,24 @@ async function main() {
],
initialValue: 'bun',
}),
+ lowerCaseName: ({ results }) => {
+ const defaultLowercase = String(results.dirName || results.projectName)
+ .trim()
+ .toLowerCase()
+ .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('.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!')}`;
+ },
+ });
+ },
faviconPath: () =>
p.text({
message: `${color.cyan('🖼️ Path to favicon SVG (leave empty to skip):')}`,
@@ -107,8 +125,41 @@ async function main() {
task: async () => {
const __dirname = path.dirname(new URL(import.meta.url).pathname);
const templateDir = path.resolve(__dirname, 'template');
+
+ // Ensure root directory exists with proper permissions
await fs.ensureDir(rootDir);
- await fs.copy(templateDir, rootDir, { overwrite: true });
+
+ try {
+ await fs.copy(templateDir, rootDir, {
+ overwrite: true,
+ errorOnExist: false,
+ preserveTimestamps: false
+ });
+ } catch (error) {
+ throw new Error(`Failed to copy template files: ${error.message}`);
+ }
+
+ // Rename gitignore files to .gitignore (dotfiles get lost in npm packages)
+ async function renameGitignoreFiles(dir) {
+ try {
+ const items = await fs.readdir(dir);
+ for (const item of items) {
+ const itemPath = path.join(dir, item);
+ const stat = await fs.stat(itemPath);
+
+ if (stat.isDirectory()) {
+ await renameGitignoreFiles(itemPath);
+ } 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}`);
+ }
+ }
+
+ await renameGitignoreFiles(rootDir);
return 'Template copied!';
},
},
@@ -205,13 +256,30 @@ async function main() {
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": "${kebabName}"`);
+ wranglerFile = wranglerFile.replace(/"name": "[^"]+"/, `"name": "${project.lowerCaseName}"`);
await fs.writeFile(wranglerPath, wranglerFile);
}
return `Sanity project created with ID: ${sanityJson.projectId}`;
},
},
+ {
+ 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}`);
+
+ try {
+ await runCommand(pmx, ['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';
+ }
+ throw new Error(`Failed to create production dataset: ${error.message}`);
+ }
+ },
+ },
{
title: `${color.green('🔐 Creating Sanity viewer token')}`,
task: async () => {
@@ -231,8 +299,8 @@ async function main() {
let content = await fs.readFile(connPath, 'utf8');
content = content
.replace(/publicViewerToken: "[^"]+"/, `publicViewerToken: "${token.key}"`)
- .replace(/studioHost: "[^"]+"/, `studioHost: "${kebabName}"`)
- .replace(/studioUrl: "[^"]+"/, `studioUrl: "https://${kebabName}.sanity.studio"`);
+ .replace(/studioHost: "[^"]+"/, `studioHost: "${project.lowerCaseName}"`)
+ .replace(/studioUrl: "[^"]+"/, `studioUrl: "https://${project.lowerCaseName}.sanity.studio"`);
await fs.writeFile(connPath, content);
return 'Viewer token created and configured!';
},
@@ -257,7 +325,7 @@ async function main() {
'http://localhost:5173',
'https://*.api.sanity.io',
'wss://*.api.sanity.io',
- `https://${kebabName}.sanity.studio`,
+ `https://${project.lowerCaseName}.sanity.studio`,
];
if (project.extraDomain && project.extraDomain.trim()) {
corsOrigins.push(`https://${project.extraDomain.trim()}`);
@@ -267,7 +335,7 @@ async function main() {
title: `${color.cyan('🌐 Adding Sanity CORS origin:')} ${color.yellow(origin)}`,
task: async () => {
const args = ['sanity', 'cors', 'add', origin, '--yes'];
- if (origin === `https://${kebabName}.sanity.studio`) args.push('--credentials');
+ if (origin === `https://${project.lowerCaseName}.sanity.studio`) args.push('--credentials');
await runCommand(pmx, args, studioDir);
return `CORS added: ${origin}` + (args.includes('--credentials') ? ' (credentials allowed)' : '');
},
@@ -290,7 +358,7 @@ async function main() {
]);
const org = typeof project.gitOrg === 'string' ? project.gitOrg.trim() : '';
if (org) {
- const githubUrl = `https://github.com/new?name=${kebabName}&owner=${org}`;
+ const githubUrl = `https://github.com/new?name=${kebabName}&owner=${org}&visibility=private&description=${encodeURIComponent('This website was built using the official Lumify starter template for building with Sveltekit, Sanity, Bun, and Shadcn UI — bundled into a NX monorepo.')}`;
p.note(
`Please create a new GitHub repository named\n\`${kebabName}\` under \`${org}\` at:\n\n${color.cyan(githubUrl)}\n\nThe browser will open for you.`,
'GitHub Setup Required'
@@ -332,7 +400,7 @@ async function main() {
` • 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://${kebabName}.sanity.studio`)} ${color.yellow('✨')}`
+ ` • Studio: ${color.cyan(`https://${project.lowerCaseName}.sanity.studio`)} ${color.yellow('✨')}`
);
process.exit(0);
}
diff --git a/package.json b/package.json
index b46fa93..7acc09a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@lumify-systems/template-sanity",
- "version": "2.0.0",
+ "version": "2.1.0",
"publishConfig": {
"access": "restricted",
"registry": "https://npm.pkg.github.com"
diff --git a/template/apps/client/.gitignore b/template/apps/client/gitignore
similarity index 100%
rename from template/apps/client/.gitignore
rename to template/apps/client/gitignore
diff --git a/template/apps/client/src/lib/asset-to-url-client.ts b/template/apps/client/src/lib/asset-to-url-client.ts
deleted file mode 100644
index b7679ee..0000000
--- a/template/apps/client/src/lib/asset-to-url-client.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { getImageDimensions, type SanityImageDimensions } from '@sanity/asset-utils';
-import type { ImageWithAlt } from './sanity.types';
-import { generateImageUrl } from './image-url';
-import { client } from './sanity';
-
-export type SimpleImage = {
- url: string;
- alt: string;
- dimensions: SanityImageDimensions;
-};
-
-// Internal helper to fetch image data from Sanity (client-side)
-async function fetchImage(assetRef: string | undefined): Promise {
- if (!assetRef) return null;
-
- const image = await client.fetch(`*[_id == $id][0]`, { id: assetRef });
- return image;
-}
-
-// Internal helper to get dimensions safely
-function getDimensions(image: ImageWithAlt | any): SanityImageDimensions {
- try {
- if (image._type === 'imageWithAlt') {
- const compatibleImage = { ...image, asset: image.asset };
- return getImageDimensions(compatibleImage as any);
- }
- return getImageDimensions(image);
- } catch {
- return { width: 1200, height: 800, aspectRatio: 1.5 };
- }
-}
-
-// Convert a single asset reference to SimpleImage (client-side)
-export async function getImageClient(assetRef: string | undefined): Promise {
- const image = await fetchImage(assetRef);
- if (!image)
- return { url: '', alt: '', dimensions: { width: 0, height: 0, aspectRatio: 1 } };
-
- const dimensions = getDimensions(image);
-
- return {
- url: generateImageUrl(image),
- alt: image.alt || '',
- dimensions
- };
-}
-
-// Convert multiple asset references to SimpleImage array (client-side)
-export async function getImagesClient(assetRefs: (string | undefined)[]): Promise {
- return Promise.all(assetRefs.map((ref) => getImageClient(ref)));
-}
diff --git a/template/apps/client/src/lib/components/blocks/sanity-button.svelte b/template/apps/client/src/lib/components/blocks/sanity-button.svelte
index e37d5a6..1025fd9 100644
--- a/template/apps/client/src/lib/components/blocks/sanity-button.svelte
+++ b/template/apps/client/src/lib/components/blocks/sanity-button.svelte
@@ -1,5 +1,5 @@