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

  1. Pass the HTML entry file as `entryName` (e.g. `index.html` or `deck.html`).
  2. Validate at the client/API boundary that the chosen file extension is .html/.htm before calling buildDeployFilePlan.
  3. 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

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


AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12). Data as JSON: /api/errors/2e2a7560742bdcce. Report an issue: GitHub.