nexu-io/open-design · error · DeployError
Only HTML files can be deployed.
Error message
Only HTML files can be deployed.
What it means
Thrown by buildDeployFilePlan in apps/daemon/src/deploy.ts:244 as `DeployError('Only HTML files can be deployed.', 400)` when the validated entry path does not match `/\.html?$/i`. The deploy model uploads a single HTML entry plus its referenced assets, so a non-HTML entry (CSS, JS, image, PDF, markdown) cannot be the deploy root. `entryPath` comes from `validateProjectPath(entryName)` which also sandboxes the path.
Source
Thrown at apps/daemon/src/deploy.ts:244
: typeof prior.lastDomainPrefix === 'string'
? normalizeCloudflareDomainPrefix(prior.lastDomainPrefix)
: '';
return {
...(lastZoneId ? { lastZoneId } : {}),
...(lastZoneName ? { lastZoneName } : {}),
...(lastDomainPrefix ? { lastDomainPrefix } : {}),
};
}
// Walk the entry HTML and any referenced CSS, producing the full set of
// files that would be uploaded for a deploy along with the lists of
// missing and invalid references. Does not throw on a partial result so
// callers can distinguish between "ready to ship" and "ready except for
// these specific issues" without parsing an error string.
export async function buildDeployFilePlan(projectsRoot: string, projectId: string, entryName: string, options: DeployOptions = {}): Promise<DeployFilePlan> {
const entryPath = validateProjectPath(entryName);
if (!/\.html?$/i.test(entryPath)) {
throw new DeployError('Only HTML files can be deployed.', 400);
}
const entry = await readProjectFile(projectsRoot, projectId, entryPath, options.metadata);
const html = entry.buffer.toString('utf8');
const entryBase = path.posix.dirname(entryPath);
const deployHtml = injectDeployHookScript(
rewriteEntryHtmlReferences(html, entryBase),
options.hookScriptUrl ?? process.env.OD_DEPLOY_HOOK_SCRIPT_URL,
);
const files = new Map<string, DeployFile>();
files.set('index.html', {
file: 'index.html',
data: Buffer.from(deployHtml, 'utf8'),
contentType: entry.mime,
sourcePath: entryPath,
});
const visited = new Set<string>([entryPath]);View on GitHub (pinned to 5be4028344)
Solutions
- Pass the HTML entry file as `entryName` (e.g. `index.html` or `deck.html`).
- Validate at the client/API boundary that the chosen file extension is .html/.htm before calling buildDeployFilePlan.
- If the project has no HTML entry, generate one (export as HTML first) before deploying.
Example fix
// before await buildDeployFilePlan(root, id, 'assets/style.css', opts); // after await buildDeployFilePlan(root, id, 'index.html', opts);
Defensive patterns
Strategy: validation
Validate before calling
function isHtmlEntry(entryName: string): boolean {
return /\.html?$/i.test(entryName);
}
if (!isHtmlEntry(entryName)) {
return sendApiError(res, 400, 'Only HTML files can be deployed.');
} Type guard
function isDeployableEntry(name: string): name is string {
return typeof name === 'string' && /\.html?$/i.test(name);
} Try / catch
try {
const plan = await buildDeployFilePlan(root, id, entryName, opts);
} catch (err) {
if (err instanceof DeployError && err.status === 400 && /HTML/i.test(err.message)) {
return res.status(400).json({ error: err.message });
}
throw err;
} Prevention
- Filter the deploy-entry file picker to .html/.htm only.
- Default the entry to the project's index.html when one exists.
- Validate the extension at the API boundary so the daemon returns 400 not 500.
When it happens
Trigger: Calling `buildDeployFilePlan(projectsRoot, projectId, entryName, options)` where `entryName` resolves to a file whose name does not end in `.html` or `.htm` (case-insensitive). Commonly the deploy route received `entry` like `style.css`, `script.js`, `slide-1.png`, or `deck.pdf`.
Common situations: User selected a non-HTML artifact (an image, CSS, or PDF) as the deploy entry in the UI; the client defaulted to the project's first file rather than the HTML entry; an export artifact (.pptx/.pdf) was mistakenly chosen for hosting.
Related errors
- Cloudflare API token is required.
- Could not deploy referenced files (${parts.join('; ')}).
- Vercel token is required.
- Cloudflare account ID is required.
- Cloudflare zone is required for a custom domain.
AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12).
Data as JSON: /api/errors/2e2a7560742bdcce.
Report an issue: GitHub.