Publishing Themes
How a theme gets from your repo to the ThreeU marketplace: bundle, submit, review, integrate.
Publishing Flow
- Build —
npm run buildproducespreview-dist/index.html(relative asset URLs,base: './'). The reviewer renders this folder in a sandbox without API access, so the theme must boot on demo data (see [Dev & Publish](/docs/cli-dev)). - Describe —
threeu.jsonwithkind,key,name,versionand a full `manifest` (≥ 3pages, non-emptylocales). See [Project Setup](/docs/cli-create). - Validate —
npx threeu publish --dry-runbundles without uploading. Check the tarball containspreview-dist/and no.envfile. - Submit —
npx threeu publish(token viaTHREEU_DEVELOPER_TOKEN). Creates a submission with statusuploaded. - Review — ThreeU runs automated checks (bundle present and ≤ 50 MB,
preview_bundle,manifest_present,manifest_pages,manifest_locales, no external API calls, …) then an editorial review. - Approve & integrate — an approved theme is registered in the marketplace catalog and merged into the hosted storefront by ThreeU. Merchants then pick it in their dashboard (public) or it is assigned to a brand (exclusive).
Review Requirements
- All pages render without errors; responsive 360 → 1920 px, no horizontal scroll
- Both declared locales actually render (AR: RTL layout, Arabic-capable font); never declare a locale the theme does not ship
- Editable components use clear, descriptive
names; no hardcoded brand-specific data - No API calls outside the SDK — every fetch goes through
threeu-sdk - Public Storefront Token used correctly (no secret tokens in a theme)
- Accessibility basics (alt text, semantic HTML, keyboard nav), WCAG AA contrast
- Product pages emit
ProductJSON-LD; FAQ pages emitFAQPage preview-dist/index.htmlpresent and renders on demo data- Clean, maintainable code; no admin/back-office or auth pages (ThreeU owns those flows)
Mergeability Contract
Approval is half the pipeline. To be integrated into the hosted storefront, the bundle must let ThreeU mount your theme inside its own brand-scoped provider:
- Named `App` export from
src/index.{js,jsx,ts,tsx}— e.g.export { default as App } from './App.jsx'. - `App` is provider-free. The host wraps
<App/>in its own<ThemeProvider>with real brand data. IfAppmounts a provider too, it double-wraps and falls back to demo data. Pattern: an injectable provider prop that defaults to a pass-through; your standalonemain.jsxinjects the demo provider, the host injects nothing. - `src/styles.css` is compiled, self-contained CSS. The host does not run your Tailwind/PostCSS. Copy Vite's emitted CSS to
src/styles.cssin a postbuild step. - `@/` aliases are fine — the integration step rewrites them to the host's path.
- Import only what the SDK exports — the host's
threeu-sdkmust export every hook/component you use.
Provider-free App
TSX
// src/App.jsx
const PassThrough = ({ children }) => children;
export default function App({ Provider = PassThrough }) {
return (
<Router>
<LanguageProvider>
<Provider><Routes /></Provider>
</LanguageProvider>
</Router>
);
}
// src/index.jsx (host-merge entry)
export { default as App } from './App.jsx';
// src/main.jsx (standalone / preview)
createRoot(root).render(<App Provider={ThemeRoot} />);Limits & Statuses
| Item | Value |
|---|---|
| Bundle size | ≤ 50 MB after exclusions (node_modules, dist, .git, .env* are dropped; preview-dist/ is kept) |
| Projects per account | Limited per developer account (new versions of an existing key do not count) |
| Key ownership | A key owned by another developer → 422 |
| Statuses | pending_upload → uploaded → approved / rejected |
| List submissions | GET /api/developer/submissions |
Each npx threeu publish creates a new submission; superseded ones can be ignored.
Common Failures
| Symptom | Cause / fix |
|---|---|
401 on publish | Token invalid/expired, or a production token used against sandbox |
422 theme key taken | Pick a new key in threeu.json |
preview_bundle fails | preview-dist/index.html missing — set build.outDir: 'preview-dist', base: './' |
manifest_present fails | threeu.json has no manifest object |
manifest_pages / manifest_locales fail | Declare ≥ 3 pages / a non-empty locales array |
| Rejected: external API calls | A non-ThreeU fetch survived (e.g. a base44 call) — grep and remove |
| Token found in the bundle | Secret file was not named .env* — rename to .env.threeu, rotate the token |