feat(embeds): add embed.classroomio.com public widget host

Serve widgets from short public URLs via tenant-router with R2-backed
assets and runtime API_URL injection, so copy-paste snippets hide infra
paths. Update widget payloads, CSP, upload purge URLs, and dev-only API
inject; document format/stage workflow in AGENTS.md and fix ui-prefix-check
YAML syntax.
This commit is contained in:
Rotimi Best
2026-06-12 02:06:37 +01:00
parent c868b2f23f
commit 367ddcddd7
23 changed files with 399 additions and 62 deletions
@@ -19,7 +19,6 @@
<script type="module">
const params = new URLSearchParams(location.search);
const key = params.get('key');
const apiBaseUrl = params.get('api');
if (!key) {
const note = document.createElement('p');
@@ -31,15 +30,12 @@
const widgetDiv = document.createElement('div');
widgetDiv.setAttribute('data-cio-widget', 'course-widget');
widgetDiv.setAttribute('data-widget-key', key);
if (apiBaseUrl) {
widgetDiv.setAttribute('data-api-base-url', apiBaseUrl);
}
mount.appendChild(widgetDiv);
const script = document.createElement('script');
script.async = true;
script.type = 'module';
script.src = './course-widget.js';
script.src = window.location.pathname;
document.body.appendChild(script);
}
</script>
@@ -3,20 +3,42 @@
import type { TWidgetPayload } from '@cio/utils/validation/widget';
import { CourseWidget } from '@cio/ui/custom/widget-layouts';
const DEFAULT_API_BASE_URL = 'https://api.classroomio.com';
declare global {
interface Window {
CIO?: {
apiBaseUrl?: string;
};
}
}
let { publicKey, apiBaseUrl = DEFAULT_API_BASE_URL }: { publicKey: string; apiBaseUrl?: string } = $props();
let { publicKey, apiBaseUrl }: { publicKey: string; apiBaseUrl?: string } = $props();
let payload = $state<TWidgetPayload | null>(null);
let errorMessage = $state('');
function resolveApiBaseUrl(): string {
if (apiBaseUrl) {
return apiBaseUrl.replace(/\/$/, '');
}
const fromWindow = typeof window !== 'undefined' ? window.CIO?.apiBaseUrl : undefined;
if (fromWindow) {
return fromWindow.replace(/\/$/, '');
}
return '';
}
async function loadPayload() {
try {
errorMessage = '';
const response = await fetch(
`${(apiBaseUrl || DEFAULT_API_BASE_URL).replace(/\/$/, '')}/widgets/${publicKey}/payload`
);
const apiOrigin = resolveApiBaseUrl();
if (!apiOrigin) {
throw new Error('Widget API URL is not configured');
}
const response = await fetch(`${apiOrigin}/widgets/${publicKey}/payload`);
const result = (await response.json()) as { success?: boolean; data?: TWidgetPayload; error?: string };
if (!response.ok || !result.success || !result.data) {