Interactive CLI to scaffold Factor1 headless CMS starter projects.
Combines your choice of CMS, frontend framework, hosting provider, and optional tooling into a ready-to-deploy project in seconds.
Supported stack options:
| Layer | Options |
|---|---|
| CMS | Sanity, Storyblok, DatoCMS, Contentful, or None (local data) |
| Frontend | Next.js 16, Astro 6 |
| Hosting | Vercel, Netlify, Cloudflare Pages, Self-hosted VPS |
| Package manager | Bun, npm, pnpm, Yarn |
| Extras | See optional features below |
No install required — use your package manager's create command:
# Bun
bun create f1-app
# npm
npm create f1-app@latest
# pnpm
pnpm create f1-app@latest
# Yarn
yarn create f1-appThe CLI walks you through all choices interactively and scaffolds the project into a new directory.
After scaffolding, your project includes:
- Framework boilerplate wired to your chosen CMS (typed client, queries, env vars) — or, with None, a typed local content module at
src/lib/content.tsand no CMS dependencies - Hosting config ready to deploy (
vercel.json,netlify.toml,wrangler.toml, or aDockerfile) - A
.env.example(or.envif you entered credentials during setup) with all required keys - Any optional tooling you selected, fully configured
- A landing page (
src/app/page.tsxorsrc/pages/index.astro) with a live style guide for your chosen UI library and CMS-specific rendering patterns (PortableText, StoryblokComponent, StructuredText, documentToReactComponents) — or, with None, a working homepage that renders your local content
git clone https://github.com/factor1/create-f1-app.git
cd create-f1-app
bun install # or npm install
bun run build # compiles TypeScript → dist/
node dist/index.jsTo run in watch mode during development:
bun run devAll extras are toggle-able at the prompt. Features marked Next.js only are silently skipped when Astro is selected.
| Feature | What it adds |
|---|---|
| Tailwind CSS v4 | CSS-first config, no tailwind.config.ts required |
| shadcn/ui | Installs packages, writes components.json, seeds CSS token set. Optionally adds Button, Card, and Input starter components. Auto-enables Tailwind. |
| Headless UI | @headlessui/react v2 — unstyled, fully accessible primitives (Dialog, Disclosure, Menu, Tab, etc.). Pairs naturally with Tailwind. |
| Radix UI | Low-level unstyled primitives (@radix-ui/react-slot, Label, VisuallyHidden). Full styling control without an opinionated design system. |
| Mantine | @mantine/core + @mantine/hooks — fully styled component library with 100+ components and its own CSS-in-JS theming. Creates src/app/providers.tsx and patches layout.tsx. Conflicts with Tailwind — the CLI prompts you to keep one. |
| ESLint | Flat config (eslint.config.mjs) with framework-aware rules (Next.js or Astro) |
| Prettier | .prettierrc + .prettierignore. Appends eslint-config-prettier when ESLint is also selected |
| TypeScript strict mode | noUncheckedIndexedAccess + exactOptionalPropertyTypes added to tsconfig.json |
| Env var validation | Next.js: src/env.ts via @t3-oss/env-nextjs + zod. Astro: astro:env schema in astro.config.mjs. Covers all CMS and selected-feature env vars |
| CMS image helpers | Per-CMS typed utility in src/lib/{cms}/image.ts (Sanity urlFor, Storyblok transform, DatoCMS srcset builder, Contentful Image API) |
| Revalidation webhook | POST /api/revalidate — validates x-revalidate-secret header, accepts { path } or { tag } body to call revalidatePath/revalidateTag. Next.js only |
| Draft/preview mode | GET /api/preview?secret=…&slug=… enables Next.js draft mode and redirects. GET /api/disable-draft exits preview. Next.js only |
| Sentry | @sentry/nextjs or @sentry/astro, SDK config files, withSentryConfig wrapper on next.config.ts, SENTRY_DSN env var |
| Analytics | Sub-prompt for provider: Vercel Analytics, PostHog, or Plausible. Injected into the root layout for both frameworks |
| Vitest | Unit test config with framework plugin (@vitejs/plugin-react for Next.js), example test and setup file |
| Playwright | E2E config wired to the correct dev port (3000/4321) and package manager run command |
| GitHub Actions | CI workflow that installs deps and runs whichever of lint, format check, unit tests, and E2E tests you selected |
| Dependabot | .github/dependabot.yml — weekly grouped dependency updates (production + development) |
| MCP config | .cursor/mcp.json + .mcp.json — wires your CMS's MCP server with env var references for AI editor integration |
| Storybook | Component explorer (experimental with Astro — prompts before enabling) |
| Provider | What's generated |
|---|---|
| Vercel Analytics | <Analytics /> injected into layout. @vercel/analytics package |
| PostHog | src/app/providers.tsx client component wrapping the app. posthog-js package. POSTHOG_KEY + POSTHOG_HOST env vars |
| Plausible | Script tag injected into layout <head>. No additional packages. PLAUSIBLE_DOMAIN env var |
Selecting shadcn installs the packages, writes components.json, and adds the CSS variable token set to your global stylesheet. It does not pre-generate component files by default — add components after init with:
npx shadcn add button
npx shadcn add card input dialog # etc.Optionally, the CLI can scaffold Button, Card, and Input starter files for you — just answer Yes when prompted. Badge, Label, and Textarea components are always generated (used by the landing page style guide).
Mantine uses its own CSS-in-JS theming system and does not work well alongside Tailwind. If you select both, the CLI will ask which to keep. Mantine setup creates:
src/app/providers.tsx—'use client'wrapper withMantineProvider- Patches
layout.tsxto import and wrap{children}with<Providers> postcss.config.mjswithpostcss-preset-mantineandpostcss-simple-vars
The CLI optionally prompts for CMS API keys during setup. If you skip this step, all required keys are written to .env.example so you can fill them in later:
cp .env.example .env
# then fill in your values| CMS | Variable(s) |
|---|---|
| Sanity (Next.js) | NEXT_PUBLIC_SANITY_PROJECT_ID, NEXT_PUBLIC_SANITY_DATASET, SANITY_API_TOKEN |
| Sanity (Astro) | PUBLIC_SANITY_PROJECT_ID, PUBLIC_SANITY_DATASET, SANITY_API_TOKEN |
| Storyblok | STORYBLOK_ACCESS_TOKEN |
| DatoCMS | DATOCMS_API_TOKEN |
| Contentful | CONTENTFUL_SPACE_ID, CONTENTFUL_ACCESS_TOKEN, CONTENTFUL_PREVIEW_TOKEN |
| None (local data) | none — content lives in src/lib/content.ts |
Feature-specific vars are added automatically when the relevant feature is selected (e.g. REVALIDATE_SECRET, PREVIEW_SECRET, NEXT_PUBLIC_SENTRY_DSN).
Rich-text renderer packages are added automatically based on your CMS and framework:
| CMS | Next.js | Astro |
|---|---|---|
| Sanity | next-sanity (includes @portabletext/react) |
@sanity/client · @portabletext/react added when a React UI lib is selected |
| Storyblok | @storyblok/react |
@storyblok/astro |
| DatoCMS | @datocms/cda-client · graphql-request · react-datocms |
@datocms/cda-client · graphql-request · react-datocms added when a React UI lib is selected |
| Contentful | contentful · @contentful/rich-text-react-renderer · @contentful/rich-text-types |
contentful · @contentful/rich-text-html-renderer · @contentful/rich-text-types |
Choosing None scaffolds a project with no CMS at all. Content lives in a typed module at src/lib/content.ts (a site object and a posts array), and the generated homepage renders it directly — so the project runs offline, out of the box, with no API keys or extra dependencies. When you're ready to go headless, swap the exports in content.ts for a CMS client; the rest of the app keeps working as long as the shapes match.
Because they have no meaning without a CMS, these features are automatically skipped when None is selected: CMS image helpers, MCP config, draft/preview mode, and the revalidation webhook. The CMS credentials prompt is also skipped.
src/
index.ts # CLI entry point (--version, --help)
cli.ts # Interactive prompt flow
scaffold.ts # Template layer composition and programmatic file generation
install.ts # Package manager install + git init
utils.ts # File walking, deep merge, template variable substitution
config.ts # CMS environment variable mappings
types.ts # Shared TypeScript types
templates/
base/ # Files copied into every project
frontend/{nextjs,astro}/ # Framework base files
cms/{sanity,storyblok,datocms,contentful,none}/{nextjs,astro}/
hosting/{vercel,netlify,cloudflare,vps}/{nextjs,astro}/
optional/{tailwind,shadcn,shadcn-components,eslint,prettier,
vitest,playwright,github-actions,storybook,
revalidation,preview,sentry,dependabot}/
Templates are composed in layers — later layers override earlier ones. Files prefixed with _ are renamed to . on copy (e.g. _gitignore → .gitignore). Files named _package.json are deep-merged into the final package.json. Some features (analytics, env-validation, cms-images, strict-ts, landing page) are generated entirely programmatically based on the selected CMS and framework.
Headless UI, Radix UI, and Mantine are injected programmatically (no template directory) — packages are merged into package.json and framework config is patched as needed.
-
Bump the version in
package.jsonfollowing semver:npm version patch # or minor / major -
Build the distributable:
bun run build
-
Verify what will be published (should include
dist/andtemplates/):npm pack --dry-run
-
Publish:
npm publish --access public
Requires being logged in to npm (
npm login) with publish access to thecreate-f1-apppackage. -
Push the version tag created by
npm version:git push && git push --tags