{"record":{"id":"2e2a7560742bdcce","repo":"nexu-io/open-design","slug":"only-html-files-can-be-deployed","errorCode":null,"errorMessage":"Only HTML files can be deployed.","messagePattern":"Only HTML files can be deployed\\.","errorType":"http","errorClass":"DeployError","httpStatus":400,"severity":"error","filePath":"apps/daemon/src/deploy.ts","lineNumber":244,"sourceCode":"      : typeof prior.lastDomainPrefix === 'string'\n        ? normalizeCloudflareDomainPrefix(prior.lastDomainPrefix)\n        : '';\n  return {\n    ...(lastZoneId ? { lastZoneId } : {}),\n    ...(lastZoneName ? { lastZoneName } : {}),\n    ...(lastDomainPrefix ? { lastDomainPrefix } : {}),\n  };\n}\n\n// Walk the entry HTML and any referenced CSS, producing the full set of\n// files that would be uploaded for a deploy along with the lists of\n// missing and invalid references. Does not throw on a partial result so\n// callers can distinguish between \"ready to ship\" and \"ready except for\n// these specific issues\" without parsing an error string.\nexport async function buildDeployFilePlan(projectsRoot: string, projectId: string, entryName: string, options: DeployOptions = {}): Promise<DeployFilePlan> {\n  const entryPath = validateProjectPath(entryName);\n  if (!/\\.html?$/i.test(entryPath)) {\n    throw new DeployError('Only HTML files can be deployed.', 400);\n  }\n\n  const entry = await readProjectFile(projectsRoot, projectId, entryPath, options.metadata);\n  const html = entry.buffer.toString('utf8');\n  const entryBase = path.posix.dirname(entryPath);\n  const deployHtml = injectDeployHookScript(\n    rewriteEntryHtmlReferences(html, entryBase),\n    options.hookScriptUrl ?? process.env.OD_DEPLOY_HOOK_SCRIPT_URL,\n  );\n  const files = new Map<string, DeployFile>();\n  files.set('index.html', {\n    file: 'index.html',\n    data: Buffer.from(deployHtml, 'utf8'),\n    contentType: entry.mime,\n    sourcePath: entryPath,\n  });\n\n  const visited = new Set<string>([entryPath]);","sourceCodeStart":226,"sourceCodeEnd":262,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/apps/daemon/src/deploy.ts#L226-L262","documentation":"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.","triggerScenarios":"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`.","commonSituations":"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.","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."],"exampleFix":"// before\nawait buildDeployFilePlan(root, id, 'assets/style.css', opts);\n\n// after\nawait buildDeployFilePlan(root, id, 'index.html', opts);","handlingStrategy":"validation","validationCode":"function isHtmlEntry(entryName: string): boolean {\n  return /\\.html?$/i.test(entryName);\n}\nif (!isHtmlEntry(entryName)) {\n  return sendApiError(res, 400, 'Only HTML files can be deployed.');\n}","typeGuard":"function isDeployableEntry(name: string): name is string {\n  return typeof name === 'string' && /\\.html?$/i.test(name);\n}","tryCatchPattern":"try {\n  const plan = await buildDeployFilePlan(root, id, entryName, opts);\n} catch (err) {\n  if (err instanceof DeployError && err.status === 400 && /HTML/i.test(err.message)) {\n    return res.status(400).json({ error: err.message });\n  }\n  throw err;\n}","preventionTips":["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."],"tags":["deploy","validation","html","file-path"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}