Custom portal pages
Publish standalone HTML and JavaScript pages through MCP, with private previews and static assets.
Custom pages are complete HTML documents at exact paths on a business's
https://<business-slug>.portal.octocom.ai portal. They have no Octocom header,
footer or React wrapper. The business's help center must be enabled.
MCP workflow
- Use
list_portal_pagesandget_portal_pageto inspect existing pages. - Optionally call
upload_portal_assetwith a filename, supported content type and canonical base64 bytes. Use the returned relative path in your HTML. - Call
save_portal_pagewithbusinessIdand apagecontainingpath,title,htmlandassetIds. This creates or replaces a draft at that path. Include the IDs of every uploaded asset the document uses. - Call
preview_portal_pagewith the returned page ID. The bearer link lasts 30 minutes and shows the latest draft. Anyone with the link can view it; keep it private. A new preview link invalidates the old one. - Call
publish_portal_pageto make the draft HTML and asset manifest public together. Editing a draft does not change the published page. unpublish_portal_pagerestores default routing but keeps the draft.delete_portal_pagepermanently removes both versions. Both revoke previews; publishing also revokes previews.
Management tools enforce the authenticated organization's business scope and existing MCP write permissions. Uploading/publishing never requires a Worker redeployment. A path is unique within its business; paths cannot be renamed in place in this first version. Save at a new path and unpublish/delete the old page.
Routing and limits
- Exact paths only, including
/,/returns,/custom/contactor a particular/article/<id>. No route parameters or catch-all templates yet. - Trailing slashes are normalized; paths are case-sensitive. Use ASCII letters, numbers, hyphens, underscores and dots within path segments. Query strings do not select pages. Encoded paths, dot segments and duplicate slashes are rejected.
/api,/assets,/_portal,/index.htmland/favicon.icoare reserved.- Without a published override,
/,/article/<id>and/category/<id>use the existing React help center. Other unmatched paths return 404. - HTML is limited to 512 KiB of UTF-8. Put JavaScript and CSS inline in the file.
- Uploaded assets are limited to 2 MiB each: PNG, JPEG, GIF, WebP, AVIF, WOFF/WOFF2 fonts and PDF downloads. HTML, JavaScript, CSS and SVG file uploads are not supported; SVG markup can be inline in your HTML.
- Uploaded bytes are immutable; replacing an asset creates a new ID and path.
Static assets
An upload returns a path such as /_portal/assets/<uuid>. Reference that path
literally, rather than assembling it dynamically, so preview can rewrite it to
its authorized preview path. Include the asset ID in the page's assetIds list.
Assets are stored privately. Public asset requests succeed only if a published page in that business references the asset. Draft previews can access assets in that draft's manifest. Disabling the portal prevents new document and asset requests. Previously downloaded content cannot be revoked from a visitor.
Use list_portal_assets to inspect uploads and delete_portal_asset to remove
unused files. Deletion is refused while any draft or published manifest refers
to the asset. Remove references and publish/unpublish the page first.
JavaScript and public APIs
Custom pages and previews run under an HTTP CSP sandbox. Inline scripts, forms,
modals, popups and downloads are allowed. Cookies, local storage, service workers
and other same-origin privileges are unavailable. Popups inherit the sandbox.
The page cannot be embedded in an iframe. External integrations may not support
the sandbox's opaque (null) origin.
Read the public portal APIs without credentials:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Our help center</title>
</head>
<body>
<h1 id="name">Help center</h1>
<script>
fetch("/api/help-center", { credentials: "omit" })
.then((response) => {
if (!response.ok) throw new Error("Help center unavailable");
return response.json();
})
.then((data) => {
document.getElementById("name").textContent = data.businessName;
});
</script>
</body>
</html>Available GET endpoints: /api/help-center, /api/help-center/search?q=...,
/api/help-center/article/<id> and /api/help-center/category/<id>. Scope comes
from the portal hostname. Do not put credentials or private data in published
HTML. Custom HTML is served to browsers, never executed on the backend.
This feature does not add custom-domain management or change the legacy Azure portal. There is no dashboard editor or full revision history in this release.