npm package: @i.sangam/create-wp-theme
Description: Scaffold a WordPress FSE theme with Vite + Tailwind v4 + Biome
Author: lamsalsangam
Latest version: 1.0.2

# create-wp-theme

A CLI scaffolding tool that generates production-ready **WordPress Full Site Editing (FSE) themes** with a modern frontend toolchain.

Combines Vite's speed, Tailwind CSS v4's simplicity, and Biome's all-in-one linting into a single zero-config starter.

## Why this exists

Existing WordPress + Tailwind starters either lack FSE support, ship with outdated tooling, or require manual configuration to get hot reload working. This tool generates a theme that:

- Works with the **WordPress Site Editor** out of the box (theme.json, block templates, template parts)
- Uses **Vite** for instant hot module replacement — no page refreshes during CSS/JS development
- Automatically switches between dev server and production builds — **zero manual file edits** to toggle environments
- Produces **deploy-ready folders and zips** that contain only the files your server needs
- Uses **class-based PHP** with a clean singleton pattern instead of a monolithic `functions.php`
- Always installs the **latest versions** of every dependency

## Quick start

With [Bun](https://bun.sh/) (recommended):

```bash
bunx @i.sangam/create-wp-theme
```

With npm:

```bash
npx @i.sangam/create-wp-theme
```

Both work identically. Bun is recommended for faster installs and builds, but Node.js 18+ works fine too.

## Interactive setup

The CLI walks you through each option one at a time. Only the **theme name** is required — everything else has smart defaults you can accept by pressing Enter.

If you leave a required field empty, the cursor stays in place and shows an inline error — no disruptive re-prompts.

URLs are auto-prefixed with `https://` if you omit the protocol (e.g. typing `example.com` becomes `https://example.com`).

```
  ┌  create-wp-theme
  │  WordPress FSE  •  Vite  •  Tailwind v4  •  Biome
  │
  ◆  Theme name *
  │  e.g. Flavor starter, flavor starter
  │  ❯ Flavor starter
  │
  ◆  Theme slug
  │  e.g. flavor_starter → folder: /flavor_starter, text-domain: flavor-starter
  │  press Enter for flavor_starter
  │  ❯
  │
  ◆  Function prefix
  │  e.g. flavor → Flavor_Theme, FLAVOR_THEME_DIR
  │  press Enter for flavor
  │  ❯
  │
  ◆  Theme URL
  │  e.g. example.com → https:// added automatically
  │  ❯
  │
  ◆  Author
  │  e.g. John Doe, Starter Inc.
  │  ❯
  │
  ◆  Author URL
  │  e.g. johndoe.com → https:// added automatically
  │  ❯
  │
  ◆  Description
  │  e.g. A minimal starter theme for client projects
  │  press Enter for Flavor starter — FSE WordPress theme
  │  ❯
  │
  ◆  Destination
  │  e.g. ./wp-content/themes → creates ./flavor_starter/ inside it
  │  press Enter for .
  │  ❯
  │
  ◇  Summary
  │
  │  Theme         Flavor starter
  │  Directory     ./flavor_starter/
  │  Slug          flavor-starter
  │  Prefix        flavor → Flavor_Theme
  │  Constants     FLAVOR_THEME_DIR, FLAVOR_THEME_URI
  │  Text Domain   flavor-starter
  │
  ◆  Create this theme?
  │  Y/n ❯ Yes
  │
  ◇  Scaffolding
  │
  │  ✓ Created directories
  │  ✓ Written theme files
  │  ✓ Dependencies installed
  │  ✓ Initial build complete
  │
  └  Done! Theme created at ./flavor_starter/
```

### What each option does

| Option | Required | What it controls |
|---|---|---|
| **Theme name** | Yes | Display name in WordPress admin (Appearance → Themes) |
| **Theme slug** | No | Folder name, translation text domain, and CSS handle prefix. Default: theme name in `snake_case` |
| **Function prefix** | No | PHP class names and constants. E.g. `flavor` becomes `Flavor_Theme`, `FLAVOR_THEME_DIR`. Default: first word of slug |
| **Theme URL** | No | Link shown in theme details. `https://` added automatically if omitted |
| **Author** | No | Author name shown in theme details |
| **Author URL** | No | Author link in theme details. Defaults to Theme URL if provided |
| **Description** | No | Short description shown in theme details. Default: `{name} — FSE WordPress theme` |
| **Destination** | No | Parent directory where the theme folder is created. Default: current directory (`.`) |

## What you get

The generated theme follows WordPress FSE conventions with a clean separation between source files (for development) and production files (for deployment):

```
my_theme/
│
│  ── WordPress theme files (production) ──────────────────
│
├── functions.php              Entry point — loads the class bootstrap
├── style.css                  Theme metadata (name, author, version, etc.)
├── theme.json                 FSE config: colors, fonts, layout, spacing
│
├── templates/                 FSE block templates
│   ├── index.html             Blog / archive listing
│   ├── page.html              Static pages
│   ├── single.html            Individual posts
│   └── 404.html               Not found page
│
├── parts/                     FSE block template parts
│   ├── header.html            Site header (navigation, logo)
│   └── footer.html            Site footer (copyright, links)
│
├── template-parts/            PHP template parts
│   └── common/                Shared partials across custom page templates
│
├── inc/                       PHP classes (clean architecture)
│   ├── class-{prefix}-theme.php    Singleton bootstrap, constants, dependency loader
│   ├── class-{prefix}-setup.php    Theme supports, nav menus, custom logo, i18n
│   └── class-{prefix}-assets.php   Asset enqueue with automatic HMR detection
│
├── assets/
│   ├── dist/                  Compiled CSS + JS (generated by Vite)
│   └── fonts/                 Self-hosted WOFF2 font files
│
│  ── Development files (not deployed) ────────────────────
│
├── src/
│   ├── main.css               Tailwind CSS v4 source + custom styles
│   └── main.js                JavaScript entry point (imports the CSS)
│
├── scripts/
│   └── bundle.js              Production bundler (creates deploy folder or zip)
│
├── vite.config.js             Vite config with Tailwind plugin + hot file management
├── biome.json                 Linter + formatter rules
├── package.json               Scripts and devDependencies
└── .gitignore                 Ignores node_modules, dist, hot, deploy output
```

## Development workflow

### 1. Install dependencies

```bash
cd my_theme
bun install       # or: npm install
```

### 2. Start the dev server

```bash
bun run dev       # or: npm run dev
```

What happens behind the scenes:
1. Vite starts and compiles your Tailwind CSS + JS
2. A `hot` file is created in the theme root
3. The PHP asset loader detects the `hot` file and loads CSS/JS from `localhost:5173` instead of `assets/dist/`
4. Vite watches for file changes and pushes updates to the browser via WebSocket

This means:
- **CSS changes** (in `src/main.css`) appear instantly — no page refresh needed
- **JS changes** (in `src/main.js`) are hot-swapped or trigger a minimal reload
- **PHP/HTML changes** require a manual browser refresh (Vite only handles CSS/JS)
- **No manual file edits** are needed to switch between dev and production mode

### 3. Edit your files

| What you want to change              | Where to edit                                |
|--------------------------------------|----------------------------------------------|
| Styles (Tailwind utilities + custom) | `src/main.css`                               |
| JavaScript                          | `src/main.js`                                |
| Theme colors, fonts, spacing, layout | `theme.json`                                 |
| Block templates                      | `templates/*.html`                           |
| Block header / footer                | `parts/header.html`, `parts/footer.html`     |
| PHP template parts                   | `template-parts/**/*.php`                    |
| Theme supports, menus, logo          | `inc/class-{prefix}-setup.php`               |
| Asset loading behavior               | `inc/class-{prefix}-assets.php`              |
| Linting / formatting rules           | `biome.json`                                 |
| Vite build configuration             | `vite.config.js`                             |

### 4. Stop the dev server

Press `Ctrl+C`. The `hot` file is automatically removed. The next time WordPress loads the page, it serves the compiled files from `assets/dist/` instead. No configuration changes needed.

## Production builds

### Build + deploy folder

```bash
bun run build       # or: npm run build
```

This runs two steps automatically:
1. **Vite compiles** Tailwind CSS + JS into `assets/dist/`
2. **Bundle script copies** only the production files into a `{theme_slug}/` folder inside the theme root

The output folder is ready to copy directly to your server's `wp-content/themes/` directory:

```
my_theme/                ← your development theme
├── my_theme/            ← copy this entire folder to your server
│   ├── assets/          (includes compiled dist/ and fonts/)
│   ├── inc/             (PHP classes)
│   ├── parts/           (block template parts)
│   ├── templates/       (block templates)
│   ├── template-parts/  (PHP partials)
│   ├── functions.php
│   ├── style.css
│   └── theme.json
```

### Build + deploy zip

```bash
bun run build:zip       # or: npm run build:zip
```

Same as above, but packages everything into `{theme_slug}.zip`. This is useful for:
- Uploading via WordPress admin (Appearance → Themes → Add New → Upload Theme)
- Distributing the theme to clients or other developers
- Storing versioned releases

### What's included vs excluded in production builds

| Included (deployed to server)                               | Excluded (development only)                   |
|-------------------------------------------------------------|-----------------------------------------------|
| `assets/` (compiled CSS/JS in `dist/` + `fonts/`)           | `node_modules/` (dependencies)                |
| `inc/` (PHP classes)                                        | `src/` (Tailwind source CSS, JS source)       |
| `parts/` (FSE block template parts)                         | `scripts/` (build tooling)                    |
| `templates/` (FSE block templates)                          | `package.json`, `bun.lock`                    |
| `template-parts/` (PHP partials)                            | `vite.config.js`, `biome.json`                |
| `functions.php`, `style.css`, `theme.json`                  | `.gitignore`, `README.md`, `hot`              |

## All commands

| Command              | What it does                                                      |
|----------------------|-------------------------------------------------------------------|
| `bun run dev`        | Start Vite dev server with hot module replacement                 |
| `bun run build`      | Compile CSS/JS and create a deploy-ready theme folder             |
| `bun run build:zip`  | Compile CSS/JS and create a deploy-ready theme zip                |
| `bun run lint`       | Check JS/CSS files for errors and issues with Biome               |
| `bun run lint:fix`   | Check and automatically fix issues with Biome                     |
| `bun run format`     | Format JS/CSS files with Biome                                    |

All commands also work with `npm run` instead of `bun run`.

## How auto HMR detection works

The theme uses a `hot` file mechanism (inspired by [Laravel Vite](https://laravel.com/docs/vite)) to automatically detect whether to load assets from the Vite dev server or from the compiled `dist/` folder:

```
Developer runs `bun run dev`
       │
       ▼
Vite starts → creates empty `hot` file in theme root
       │
       ▼
WordPress loads a page → PHP checks: does `hot` file exist?
       │
   ┌───┴───┐
   YES     NO
   │       │
   ▼       ▼
Enqueue from        Enqueue from
localhost:5173      assets/dist/
(live dev server)   (compiled files)
       │
       ▼
Developer stops Vite (Ctrl+C)
       │
       ▼
Vite plugin removes `hot` file automatically
       │
       ▼
Next page load → `hot` file gone → serves from dist/
```

This is handled by two files working together:
- **`vite.config.js`** — A custom Vite plugin creates the `hot` file when the dev server starts and removes it when the server stops
- **`class-{prefix}-assets.php`** — The `is_vite_dev()` method checks for the `hot` file and routes asset loading accordingly

You never need to manually edit either file to switch between development and production modes.

## PHP architecture

The theme avoids the common WordPress anti-pattern of a monolithic `functions.php` with hundreds of lines of unrelated code. Instead, it uses a **class-based singleton pattern** with separated concerns:

```
functions.php
  └── requires and boots {Prefix}_Theme (singleton)
        │
        ├── define_constants()
        │     ├── {PREFIX}_THEME_VERSION  → Theme version from style.css
        │     ├── {PREFIX}_THEME_DIR      → Absolute server path to theme root
        │     └── {PREFIX}_THEME_URI      → URL to theme root
        │
        ├── load_dependencies()
        │     ├── class-{prefix}-setup.php
        │     └── class-{prefix}-assets.php
        │
        └── register_hooks()
              ├── {Prefix}_Setup::init()
              │     └── after_setup_theme → supports, menus, logo, i18n
              │
              └── {Prefix}_Assets::init()
                    ├── wp_enqueue_scripts → auto HMR or production assets
                    └── script_loader_tag  → adds type="module" for Vite
```

### Adding new functionality

To add a new feature (e.g. custom post types, widgets, REST API endpoints):

1. Create a new class file: `inc/class-{prefix}-{feature}.php`
2. Follow the same pattern — a `final class` with a `public static function init()` that registers WordPress hooks
3. Require it in `class-{prefix}-theme.php`'s `load_dependencies()` method
4. Call `{Prefix}_{Feature}::init()` in the `register_hooks()` method

Example:

```php
// inc/class-my-custom-posts.php
final class My_Custom_Posts {
    public static function init(): void {
        add_action( 'init', [ __CLASS__, 'register' ] );
    }

    public static function register(): void {
        register_post_type( 'portfolio', [ /* ... */ ] );
    }
}
```

## Tailwind CSS v4

The theme uses Tailwind CSS v4's **CSS-first configuration**. There is no `tailwind.config.js` file. All customization happens directly in `src/main.css` using the `@theme` directive:

```css
@import "tailwindcss";

@theme {
  --font-heading: "Inter", sans-serif;
  --font-body: "Noto Sans JP", sans-serif;
  --color-primary: #0073aa;
  --color-secondary: #23282d;
}

/* Custom component styles */
.btn-primary {
  @apply bg-primary text-white px-6 py-3 rounded-lg font-semibold
         hover:bg-primary/90 transition-colors;
}
```

Tailwind automatically scans all `.php`, `.html`, and `.js` files in the theme directory for class names and generates only the CSS you actually use.

For more details, see the [Tailwind CSS v4 documentation](https://tailwindcss.com/docs).

## Adding custom fonts

WordPress FSE themes load fonts through `theme.json`, not CSS `@font-face` rules. This ensures fonts work in both the front end and the block editor.

### Step 1: Add font files

Place your `.woff2` font files in `assets/fonts/`:

```
assets/fonts/
├── Inter-Regular.woff2
├── Inter-Bold.woff2
└── NotoSansJP-Variable.woff2
```

### Step 2: Register in theme.json

Add a `fontFace` entry under `settings.typography.fontFamilies`:

```json
{
  "fontFamily": "\"Inter\", sans-serif",
  "name": "Inter",
  "slug": "inter",
  "fontFace": [
    {
      "fontFamily": "Inter",
      "fontWeight": "400",
      "fontStyle": "normal",
      "fontDisplay": "swap",
      "src": ["file:./assets/fonts/Inter-Regular.woff2"]
    },
    {
      "fontFamily": "Inter",
      "fontWeight": "700",
      "fontStyle": "normal",
      "fontDisplay": "swap",
      "src": ["file:./assets/fonts/Inter-Bold.woff2"]
    }
  ]
}
```

For variable fonts, use a weight range:

```json
{
  "fontWeight": "100 900",
  "src": ["file:./assets/fonts/NotoSansJP-Variable.woff2"]
}
```

### Step 3: Make it available in Tailwind

Add the font to `@theme` in `src/main.css`:

```css
@theme {
  --font-inter: "Inter", sans-serif;
}
```

### Step 4: Use it

In templates: `class="font-inter"`

In `theme.json` styles:
```json
"typography": {
  "fontFamily": "var(--wp--preset--font-family--inter)"
}
```

## Adding custom page templates

The block editor handles most pages, but some pages need PHP logic — dynamic data, API calls, complex layouts with sliders or carousels, or integration with third-party services.

### Step 1: Create the PHP template

Add a PHP file at the theme root following WordPress's [template hierarchy](https://developer.wordpress.org/themes/templates/template-hierarchy/):

| File name | When WordPress uses it |
|---|---|
| `front-page.php` | Static front page (Settings → Reading → "A static page") |
| `page-{slug}.php` | Specific page by slug (e.g. `page-about.php` for `/about/`) |
| `single-{post-type}.php` | Custom post type single view |
| `archive-{post-type}.php` | Custom post type archive |

PHP templates at the root **take precedence** over block templates in `templates/`.

### Step 2: Add template parts

Create partials in `template-parts/` to keep your template organized:

```
template-parts/
├── common/           Shared across pages (header, footer)
└── frontpage/        Front page specific
    ├── hero.php
    ├── features.php
    └── testimonials.php
```

Use `get_template_part()` to include them:

```php
<?php get_template_part( 'template-parts/frontpage/hero' ); ?>
```

### Step 3: Add page-specific CSS/JS (if needed)

If the page needs its own styles or scripts beyond `main.css`/`main.js`:

1. Create new source files (e.g. `src/frontpage/frontpage.js` + `src/frontpage/frontpage.css`)
2. Add the entry point to `vite.config.js`:

```js
rollupOptions: {
  input: {
    main: resolve(__dirname, 'src/main.js'),
    frontpage: resolve(__dirname, 'src/frontpage/frontpage.js'),  // new
  },
}
```

3. Enqueue conditionally in `class-{prefix}-assets.php`:

```php
if ( is_front_page() ) {
    wp_enqueue_style( '{slug}-frontpage', $dist_uri . '/frontpage.css', [], ... );
    wp_enqueue_script( '{slug}-frontpage', $dist_uri . '/frontpage.js', [], ... );
}
```

## Requirements

| Requirement | Minimum version | Notes |
|---|---|---|
| [Bun](https://bun.sh/) or [Node.js](https://nodejs.org/) | Bun 1+ / Node 18+ | Bun recommended for speed; both work |
| PHP | 8.0+ | Required by WordPress and the generated theme |
| WordPress | 6.4+ | Required for full FSE support (theme.json v3, block templates) |

## Inspiration

This project was inspired by [\_tw](https://underscoretw.com/) and [TailPress](https://tailpress.io/) — both great tools for WordPress + Tailwind development. `create-wp-theme` builds on that foundation with a focus on WordPress FSE, modern tooling, and a streamlined developer experience.

## Features at a glance

| Feature | |
|---|---|
| WordPress FSE (theme.json, block templates) | Yes |
| Tailwind CSS v4 (CSS-first config) | Yes |
| Vite (fast builds) | Yes |
| Auto HMR (no manual dev/prod switching) | Yes |
| Biome (linter + formatter) | Yes |
| Deploy-ready builds (folder + zip) | Yes |
| Class-based PHP architecture | Yes |
| Interactive CLI scaffolding | Yes |
| Always latest package versions | Yes |
| Works with Bun and npm | Yes |

## Acknowledgments

This tool was crafted collaboratively with [Claude Code](https://claude.ai/claude-code) by Anthropic — from architecture decisions and code generation to documentation. Every pattern in the scaffolding (auto HMR detection, class-based PHP, production bundling, the interactive CLI itself) was iteratively designed and refined through that partnership to be as performant and efficient as possible.

## License

MIT