create-site-starter
v1.0.0
Published
Interactive CLI that scaffolds a production-grade Next.js App Router site with an embedded Sanity Studio, modular sections, mock-data fallback, and a pre-built legal compliance suite.
Maintainers
Readme
create-site-starter
An interactive CLI that scaffolds a production-grade Next.js (App Router) website wired to an embedded Sanity Studio, modular UI sections, a graceful mock-data fallback engine, and a pre-built legal compliance suite (cookie consent + auto-generated policy pages).
npx create-site-starterRepository: https://github.com/JovanTanchevski/SanityTemplateProjects
Table of Contents
- What You Get
- Quick Start
- The Interactive Flow
- Demo Mode & Fallback Architecture
- Connecting Sanity
- Single-Page vs Multi-Page Layout
- Modular Sections
- Legal Compliance Suite
- Local Development
- Generated Project Architecture
- Environment Variables
- Deployment
- Repository Layout & Contributing
- Tech Stack & Versions
- Troubleshooting
- License
What You Get
| Layer | Included |
| --- | --- |
| Framework | Next.js App Router, React 19, TypeScript |
| Styling | Tailwind CSS v4, CSS-variable design tokens, dark-mode aware |
| Motion & Icons | Framer Motion transitions, Lucide icon set |
| CMS | Sanity v3-generation Studio embedded at /studio, GROQ query layer |
| Resilience | Dual-mode data engine — live Sanity content, automatic mock-data fallback |
| Legal | Cookie consent banner with script gating, /privacy-policy, /terms-of-service, /cookie-policy |
| Forms | Contact form endpoint ready for Web3Forms / Resend |
| DX | Typed content models, ESLint config, zero-config npm run dev |
Every generated project builds and renders on the first npm run dev, with or without Sanity credentials. No white screens, no unhandled fetch errors.
Quick Start
# 1. Scaffold
npx create-site-starter
# 2. Enter the generated directory
cd my-business-site
# 3. Install (skip if the CLI already installed for you)
npm install
# 4. Run
npm run devThen open:
- http://localhost:3000 — the website
- http://localhost:3000/studio — the embedded Sanity Studio
The Interactive Flow
The CLI asks six questions and nothing else:
| # | Prompt | Purpose |
| --- | --- | --- |
| 1 | Project name | Directory name + package.json name (kebab-cased, validated) |
| 2 | Company / legal entity name | Injected into legal pages, footer, and metadata |
| 3 | Contact email | Contact form recipient + legal notice address |
| 4 | Layout mode | Single-Page smooth-scroll vs Multi-Page routes |
| 5 | Enabled sections | Hero, About, Person Cards, Pricing, Contact (multi-select) |
| 6 | Sanity credentials | Project ID + dataset, or skip for now |
Answers are written into .env.local, site.config.ts, and the generated route/navigation structure. Unselected sections are not scaffolded — you get no dead files.
Demo Mode & Fallback Architecture
The template never depends on a reachable CMS to render.
┌──────────────────────────────┐
page.tsx ──► │ lib/sanity/client.ts │
│ getContent(query, fallback) │
└───────────┬──────────────────┘
│
NEXT_PUBLIC_DEMO_MODE=true ──────────► mockData.ts
│
live fetch
│
┌─────────────┴─────────────┐
│ │
success + non-empty error / empty / timeout
│ │
Sanity content mockData.ts (+ console warning)How demo mode is triggered:
- The user skips the Sanity step in the CLI, or
- The credential validation request to
https://<projectId>.api.sanity.iofails.
In both cases the CLI writes NEXT_PUBLIC_DEMO_MODE=true into .env.local and prints a non-blocking warning — generation always completes successfully.
Runtime fallback is independent of the CLI: even with valid credentials, any query that throws, times out, or returns an empty result silently falls back to lib/sanity/mockData.ts so a half-populated dataset can never blank a page.
To go live later, set real credentials and flip the flag:
NEXT_PUBLIC_DEMO_MODE=falseConnecting Sanity
You can connect during generation or at any point afterwards.
1. Create a project
npx sanity@latest login
npx sanity@latest projects createOr use https://www.sanity.io/manage and copy the Project ID plus a dataset name (production by default).
2. Fill in .env.local
NEXT_PUBLIC_SANITY_PROJECT_ID=abc12xyz
NEXT_PUBLIC_SANITY_DATASET=production
NEXT_PUBLIC_SANITY_API_VERSION=2026-01-01
NEXT_PUBLIC_DEMO_MODE=false
SANITY_API_READ_TOKEN=3. Allow the Studio origin
In https://www.sanity.io/manage → API → CORS origins, add http://localhost:3000 (and your production domain) with credentials allowed.
4. Publish your first documents
Visit /studio, fill in the Hero, About, Pricing, and Contact singletons plus any Person documents, and hit Publish. The front end picks them up on the next request; anything left unpublished keeps rendering its mock counterpart.
Single-Page vs Multi-Page Layout
The choice is stored once in site.config.ts and drives navigation, routing, and scroll behaviour:
export const siteConfig = {
layoutMode: "single" as "single" | "multi",
// ...
};| | Single-Page | Multi-Page |
| --- | --- | --- |
| Navigation links | #about, #pricing, #contact | /about, /pricing, /contact |
| Scroll behaviour | Smooth anchor scrolling with header offset | Standard route transitions |
| Section rendering | All enabled sections stacked on / | One section per dedicated route |
| Best for | Landing pages, single-offer businesses | Content-rich sites, SEO per topic |
Switching modes after generation is a one-line change — the header, footer, and section wrappers all read from the same flag.
Modular Sections
| Section | Component | Sanity type |
| --- | --- | --- |
| Hero | components/sections/HeroSection.tsx | hero |
| About | components/sections/AboutSection.tsx | about |
| Team / Person Cards | components/sections/PersonCardsSection.tsx | person |
| Pricing | components/sections/PricingSection.tsx | pricing |
| Contact | components/sections/ContactSection.tsx | contact |
Each section is a self-contained server component that receives typed content and renders motion-enhanced client children. Adding a section back after generation means restoring its component, its schema entry, and one nav item — no framework surgery.
Legal Compliance Suite
Generated sites ship with consent handling wired in, not bolted on.
Cookie banner — components/legal/CookieBanner.tsx
- Appears on first visit only.
- Three actions: Accept All, Essential Only, Preferences (granular per-category toggles).
- Persists the decision in
localStorageundercookie-consent. - Broadcasts a
cookie-consent-changeevent so analytics/marketing scripts can mount only after consent. - Non-essential scripts stay unloaded until the matching category is granted.
Auto-generated policy pages — app/(legal)/
| Route | Contents |
| --- | --- |
| /privacy-policy | Data controller identity, lawful basis, retention, data subject rights, contact channel |
| /terms-of-service | Usage terms, IP ownership, warranty and liability disclaimers, governing law placeholder |
| /cookie-policy | Itemised essential vs. optional cookie table, consent withdrawal instructions |
Every page is auto-filled with the company legal entity name and contact email captured by the CLI, plus a generated "last updated" date.
Not legal advice. These documents are a solid, structured starting point that removes the blank-page problem. Have counsel review them against your jurisdiction and actual data processing before launch.
Local Development
Inside a generated project:
npm run dev # Next.js dev server + Studio at /studio
npm run build # Production build
npm run start # Serve the production build
npm run lint # ESLint
npm run typecheck # TypeScript, no emitWorking on the generator itself:
git clone https://github.com/JovanTanchevski/SanityTemplateProjects.git
cd SanityTemplateProjects
npm install
node bin/index.mjs # run the CLI from sourceGenerated Project Architecture
my-business-site/
├── app/
│ ├── (site)/ # public pages, wrapped in header + footer
│ │ ├── (legal)/
│ │ │ ├── cookie-policy/page.tsx
│ │ │ ├── privacy-policy/page.tsx
│ │ │ └── terms-of-service/page.tsx
│ │ ├── about/page.tsx # multi-page mode only
│ │ ├── contact/page.tsx # multi-page mode only
│ │ ├── pricing/page.tsx # multi-page mode only
│ │ ├── layout.tsx # site chrome
│ │ └── page.tsx # "/"
│ ├── studio/[[...tool]]/page.tsx # embedded Sanity Studio, full viewport
│ ├── globals.css
│ └── layout.tsx # root: html, body, metadata
├── components/
│ ├── legal/CookieBanner.tsx
│ ├── sections/
│ │ ├── AboutSection.tsx
│ │ ├── ContactSection.tsx
│ │ ├── HeroSection.tsx
│ │ ├── PersonCardsSection.tsx
│ │ └── PricingSection.tsx
│ ├── Footer.tsx
│ └── Header.tsx
├── lib/
│ ├── sanity/
│ │ ├── client.ts # dual-mode data engine
│ │ ├── image.ts
│ │ ├── mockData.ts
│ │ └── queries.ts # GROQ
│ └── utils.ts
├── sanity/
│ └── schemas/{hero,about,person,pricing,contact,index}.ts
├── sanity.config.ts
├── site.config.ts # CLI answers land here
├── .env.local
└── package.jsonEnvironment Variables
| Variable | Required | Description |
| --- | --- | --- |
| NEXT_PUBLIC_SANITY_PROJECT_ID | For live mode | Sanity project ID from sanity.io/manage |
| NEXT_PUBLIC_SANITY_DATASET | For live mode | Dataset name, usually production |
| NEXT_PUBLIC_SANITY_API_VERSION | Yes | Pinned API date, e.g. 2026-01-01 |
| NEXT_PUBLIC_DEMO_MODE | Yes | true forces mock data and bypasses all network calls |
| NEXT_PUBLIC_SITE_URL | Recommended | Canonical URL for metadata and Open Graph tags |
| SANITY_API_READ_TOKEN | Optional | Server-only token for private datasets and drafts |
| RESEND_API_KEY | Optional | Enables contact-form delivery via Resend |
| WEB3FORMS_ACCESS_KEY | Optional | Enables contact-form delivery via Web3Forms |
| CONTACT_TO_EMAIL | Optional | Overrides the recipient (defaults to the CLI contact email) |
| CONTACT_FROM_EMAIL | Optional | Verified sender address, Resend only |
With neither mail provider configured, /api/contact validates and accepts submissions but reports honestly that delivery is not wired up — it never silently drops a message.
.env.local is git-ignored in generated projects. Only NEXT_PUBLIC_* values reach the browser — never put a write token in one.
Deployment
The template is a stock Next.js app and deploys anywhere Next runs. On Vercel:
- Push the generated project to a Git repository and import it.
- Add every variable from
.env.localto the project's environment settings. - Set
NEXT_PUBLIC_DEMO_MODE=falseonce real content is published. - Add the production domain to Sanity's CORS origins so
/studioworks in production.
Repository Layout & Contributing
This repository contains the generator, not a generated site:
SanityTemplateProjects/
├── bin/index.mjs # CLI entry point (@clack/prompts)
├── templates/default/ # the scaffolded template source
├── package.json
└── README.mdThe template stack (sanity, next-sanity, @sanity/image-url, lucide-react, framer-motion) is mirrored in this package's devDependencies as the single source of truth for the versions written into generated projects. The CLI itself ships only @clack/prompts, chalk, and execa as runtime dependencies, so npx create-site-starter stays fast.
Issues and pull requests: https://github.com/JovanTanchevski/SanityTemplateProjects/issues
Tech Stack & Versions
| Package | Version | | --- | --- | | next | ^16.3.1 | | react / react-dom | ^19.2.8 | | typescript | ^6.0.3 | | tailwindcss | ^4.3.3 | | sanity | ^6.9.2 | | next-sanity | ^13.3.3 | | @sanity/client | ^7.26.2 | | @sanity/image-url | ^2.1.1 | | framer-motion | ^13.1.0 | | lucide-react | ^1.31.0 | | eslint | ^9.39.5 | | @clack/prompts | ^1.7.0 |
Requires Node.js >= 20.9.0.
TypeScript is pinned to the 6.x line and ESLint to 9.x on purpose: eslint-config-next bundles typescript-eslint and eslint-plugin-react, which do not yet support TypeScript 7 or ESLint 10. Generated projects therefore get a npm run lint and npm run typecheck that both pass out of the box.
Troubleshooting
/studio shows a CORS error — add your origin under sanity.io/manage → API → CORS origins with credentials allowed.
The site shows placeholder copy after connecting Sanity — NEXT_PUBLIC_DEMO_MODE is still true, or the documents exist as drafts only. Publish them and restart the dev server; NEXT_PUBLIC_* variables are inlined at build time.
Configuration must contain 'projectId' — .env.local is missing or the dev server was started before it existed. Restart after saving the file.
Sections are missing — they were deselected during generation. Restore the component under components/sections/, register its schema, and add the nav entry.
License
MIT © Jovan Tanchevski — see LICENSE.
