Official templates for the Uniweb CLI.
This repository contains the official templates for the Uniweb CLI. Templates are distributed via GitHub Releases, not npm.
When users run uniweb create my-project --template marketing, the CLI:
- Fetches the
manifest.jsonfrom the latest GitHub release - Downloads the template tarball (e.g.,
marketing.tar.gz) - Extracts and applies the template with Handlebars processing
See the templates in action: View all demos
| Template | Demo | Description |
|---|---|---|
| marketing | Live Demo | Landing pages and marketing sites |
| academic | Live Demo | Research labs and academic portfolios |
| docs | Live Demo | Documentation sites with versioning |
| international | Live Demo | Multi-language sites with i18n |
# Use an official template (Tailwind v4 by default)
npx uniweb@latest create my-project --template marketing
# Use with Tailwind v3 (legacy)
npx uniweb@latest create my-project --template marketing --variant tailwind3
# Templates are resolved in order:
# 1. Built-in (single, multi) - in CLI
# 2. Official (this repo) - marketing, docs, learning
# 3. npm packages - @scope/template-name
# 4. GitHub repos - github:user/repoEach template lives in templates/<name>/ with this structure:
templates/
└── marketing/
├── template.json # Required: Template metadata
├── preview.png # Optional: Preview image
└── template/ # Required: Files to scaffold
├── package.json.hbs
├── pnpm-workspace.yaml
├── .gitignore
├── README.md.hbs
├── foundation/
│ ├── package.json.hbs
│ ├── src/
│ │ ├── index.js
│ │ ├── styles.css
│ │ └── components/
│ │ └── Hero/
│ │ ├── index.jsx
│ │ └── meta.js
│ └── ...
└── site/
├── package.json.hbs
├── site.yml.hbs
├── vite.config.js
├── main.js
└── pages/
└── home/
├── page.yml
└── 1-hero.md
Required metadata file at the template root:
{
"name": "Marketing Starter",
"description": "A complete marketing site with landing page components",
"uniweb": ">=0.2.0",
"preview": "preview.png",
"tags": ["marketing", "landing-page", "saas"],
"components": ["Hero", "Features", "Pricing", "Testimonials", "CTA"]
}| Field | Required | Description |
|---|---|---|
name |
Yes | Human-readable template name |
description |
No | Longer description for discovery |
uniweb |
No | Semver range for Uniweb compatibility |
preview |
No | Preview image filename |
tags |
No | Keywords for discovery |
components |
No | List of included components |
Files ending in .hbs are processed through Handlebars. The .hbs extension is removed in the output.
Available variables:
{{projectName}}- Project name from CLI{{year}}- Current year- Custom variables can be passed via the API
Version helper:
Available partials:
The CLI provides shared partials that remote templates can use in .hbs files:
| Partial | Purpose |
|---|---|
{{> agents-md}} |
Complete AGENTS.md for AI assistants |
{{> search-docs}} |
Search feature documentation |
{{> exports-js}} |
JSDoc header for foundation exports.js |
Example: AGENTS.md.hbs
The agents-md partial generates a complete AI assistant guide covering project structure (both single and multi-site), component discovery, content authoring, and troubleshooting:
This teaches Claude how to explore the codebase rather than listing specific components, so the documentation never becomes outdated as the project evolves.
Example: package.json.hbs
{
"name": "{{projectName}}",
"version": "0.1.0",
"private": true
}Supported file types for processing:
.js,.jsx,.ts,.tsx,.mjs,.cjs.json,.yml,.yaml.md,.mdx.html,.htm,.css,.scss.txt,.xml,.svg,.vue,.astro
Binary files (images, fonts, etc.) are copied as-is.
The site package MUST include the foundation as a local dependency:
// site/package.json.hbs
{
"name": "site",
"dependencies": {
"@uniweb/runtime": "^0.1.0",
"foundation": "file:../foundation"
}
}This "foundation": "file:../foundation" entry is essential because:
- Both npm and pnpm create a symlink at
site/node_modules/foundationpointing to the sibling foundation - Vite's
#foundationalias resolves to thefoundationmodule - Without this, the site build will fail with "Could not load foundation"
Important: Use file: protocol, not workspace:*. The workspace:* protocol is pnpm-specific and will fail with npm.
Use fixed package names:
- Foundation:
"name": "foundation"(not{{projectName}}-foundation) - Site:
"name": "site"(not{{projectName}}-site)
The foundation package MUST include @uniweb/core as a direct dependency:
// foundation/package.json.hbs
{
"name": "foundation",
"dependencies": {
"@uniweb/core": "{{version "@uniweb/core"}}",
"@uniweb/kit": "{{version "@uniweb/kit"}}"
}
}This is required because:
- The foundation build externalizes
@uniweb/core(it's not bundled intofoundation.js) - When the site's prerender (SSG) loads
foundation/dist/foundation.jsin Node.js, it needs to resolveimport ... from '@uniweb/core' - pnpm uses strict module resolution - each package can only see its own dependencies
- Even though
@uniweb/kitdepends on@uniweb/core, the foundation's direct imports won't resolve through transitive dependencies
Without this, prerendering will fail with:
Cannot find package '@uniweb/core' imported from foundation/dist/foundation.js
The root package.json.hbs must include workspace configuration for both npm and pnpm:
{
"name": "{{projectName}}",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "pnpm --filter site dev",
"dev:runtime": "VITE_FOUNDATION_MODE=runtime pnpm --filter site dev",
"build": "pnpm -r build",
"preview": "pnpm --filter site preview"
},
"workspaces": [
"site",
"foundation",
"sites/*",
"foundations/*"
],
"pnpm": {
"onlyBuiltDependencies": ["esbuild", "sharp"]
}
}Required fields:
workspaces- Enables npm workspace linking (required forfile:dependencies to work)pnpm.onlyBuiltDependencies- Prevents pnpm from rebuilding native modules on every install
This matches how the CLI's built-in templates work and ensures the workspace linking functions correctly.
The root pnpm-workspace.yaml should include both packages:
packages:
- 'site'
- 'foundation'
- 'sites/*'
- 'foundations/*'Templates can include variant-specific directories using the .variant suffix:
template/
├── foundation/
├── foundation.typescript/ # Used when variant='typescript'
└── site/
When applying with variant: 'typescript', the foundation.typescript/ directory contents will be used instead of foundation/.
Templates are validated when applied:
- Structure check -
template.jsonandtemplate/directory must exist - Required fields -
namefield is required in template.json - Version compatibility - If
uniwebfield is set, checks against current version - Unresolved placeholders - Warns if
{{variables}}remain after processing
Add E2E tests in the main workspace at tests/e2e/<template>-template.test.js:
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { createTempDir, cleanupTempDir, runCreate, installDependencies, buildProject } from './helpers.js'
describe('Marketing Template Build', () => {
let tempDir, projectDir
beforeAll(async () => {
tempDir = await createTempDir()
projectDir = join(tempDir, 'test-project')
await runCreate('test-project', { cwd: tempDir, template: 'marketing' })
await patchForLocalPackages(projectDir)
installDependencies(projectDir)
}, 300000)
it('should build foundation successfully', () => {
buildProject(join(projectDir, 'foundation'))
expect(existsSync(join(projectDir, 'foundation/dist/foundation.js'))).toBe(true)
})
it('should build site successfully', () => {
buildProject(join(projectDir, 'site'))
expect(existsSync(join(projectDir, 'site/dist/index.html'))).toBe(true)
})
})Key testing points:
- Project scaffolds correctly with all expected files
- Foundation builds and produces
schema.jsonwith component metadata - Site builds and produces
site-content.jsonwith parsed content - Content sections are in correct order
- Handlebars variables are properly substituted
Templates are released via GitHub Releases. The release workflow is automated via GitHub Actions.
-
Make your changes to templates in
templates/ -
Commit your changes:
git add . git commit -m "feat: add new component to marketing template"
-
Bump the version (this creates a git tag):
pnpm version patch # 0.1.7 → 0.1.8 # or pnpm version minor # 0.1.7 → 0.2.0 # or pnpm version major # 0.1.7 → 1.0.0
-
Push the commit and tag:
git push && git push --tags -
GitHub Actions automatically:
- Creates tarballs for each template (e.g.,
marketing.tar.gz) - Updates
manifest.jsonwith the new version - Creates a GitHub Release with all assets
- Creates tarballs for each template (e.g.,
The workflow creates:
manifest.json- Template metadata with version<template-name>.tar.gz- One tarball per template
Each tarball contains the full template directory structure that the CLI extracts and processes.
The manifest.json file lists all available templates:
{
"version": "v0.1.8",
"templates": {
"marketing": {
"name": "Marketing Starter",
"description": "Landing pages and marketing sites with Tailwind CSS v4",
"variants": ["tailwind3"],
"tags": ["marketing", "landing-page", "tailwind"]
}
}
}- Create the template directory:
templates/<name>/ - Add
template.jsonandtemplate/directory - Add the template to
manifest.json - Test locally with the CLI
- Follow the release process above
If you see an error like:
The requested module 'react-dom/client.js' does not provide an export named 'createRoot'
This is a Vite CJS/ESM interop issue. React's npm packages (both 18 and 19) use CommonJS internally, and Vite needs to pre-bundle them to convert to ESM for the browser.
Solution: Ensure your vite.config.js includes:
export default defineConfig({
// ... other config
optimizeDeps: {
include: ['react', 'react-dom', 'react-dom/client', 'react-router-dom'],
},
})This forces Vite to pre-bundle these packages, ensuring proper CJS→ESM conversion. This is particularly important in:
- pnpm monorepos with nested
node_modulesstructures - Projects with linked/workspace dependencies
- Fresh installs before Vite's dependency discovery runs
Why does React use CommonJS? React's npm package exports CommonJS for Node.js compatibility. The ESM-only change in React 19 only affects UMD builds (for CDN script tags), not the npm package.
- Create
templates/<name>/template.jsonwith requirednamefield - Create
templates/<name>/template/directory structure - Include root
package.json.hbswithworkspacesarray andpnpm.onlyBuiltDependencies - Include
pnpm-workspace.yamlwith workspace packages - Include both
foundation/andsite/packages - Add
"foundation": "file:../foundation"to site's dependencies (notworkspace:*) - Add
"@uniweb/core"to foundation's dependencies (required for prerender/SSG) - Use fixed names:
"name": "foundation"and"name": "site" - Add
.hbsextension to files needing variable substitution - Include sample content in
site/pages/ - Create meaningful component metadata in
foundation/src/components/*/meta.js - Add E2E tests in the main workspace
- Test the full flow: create, install, build foundation, build site
MIT