Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
</head>
<body>
<div id="root">
<!-- share-html:fallback:start -->
<main>
<h1>Share HTML — upload one HTML file, get a sandboxed shareable link</h1>
<p>Upload a single self-contained HTML file and get a public, sandboxed preview URL. No signup required; anonymous uploads are kept for 365 days.</p>
Expand All @@ -39,6 +40,7 @@ <h2>Use it programmatically</h2>
# returns JSON: { "share": { "share_url", "preview_url", "slug", "id" }, "claimToken" }</pre>
<p>Full API: <a href="/openapi.json">/openapi.json</a> · AI guide: <a href="/llms.txt">/llms.txt</a> · MCP: <a href="/mcp">/mcp</a></p>
</main>
<!-- share-html:fallback:end -->
</div>
<script>
(() => {
Expand Down
39 changes: 35 additions & 4 deletions src/worker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ export default {
const assetResponse = await env.ASSETS.fetch(request);
const contentType = assetResponse.headers.get("content-type") ?? "";
if (contentType.includes("text/html") && request.method === "GET") {
return withDiscoveryHeaders(await injectConfig(assetResponse, env));
return withDiscoveryHeaders(await injectConfig(assetResponse, env, request));
}
return withDiscoveryHeaders(assetResponse);
} catch (error) {
Expand All @@ -158,20 +158,52 @@ export default {
* can initialise the Supabase client without a separate /api/config fetch.
* The injected script sets window.__APP_CONFIG__ before any module scripts run.
*/
async function injectConfig(response: Response, env: Env): Promise<Response> {
async function injectConfig(response: Response, env: Env, request: Request): Promise<Response> {
const configScript = `<script>window.__APP_CONFIG__=${JSON.stringify({
supabaseUrl: env.SUPABASE_URL,
supabasePublishableKey: env.SUPABASE_PUBLISHABLE_KEY
})}</script>`;
const html = await response.text();
const injected = html.replace("</head>", `${configScript}</head>`);
const bodyAdjusted = routeFallbackHtml(html, request);
const injected = bodyAdjusted.replace("</head>", `${configScript}</head>`);
return new Response(injected, {
status: response.status,
statusText: response.statusText,
headers: response.headers
});
}

function routeFallbackHtml(html: string, request: Request): string {
const pathname = new URL(request.url).pathname;
if (!isShareRoute(pathname)) return html;
return replaceAppFallback(html, shareRouteFallback());
}

function isShareRoute(pathname: string): boolean {
return /^\/s\/[^/]+\/?$/.test(pathname);
}

function replaceAppFallback(html: string, replacement: string): string {
const startMarker = "<!-- share-html:fallback:start -->";
const endMarker = "<!-- share-html:fallback:end -->";
const start = html.indexOf(startMarker);
const end = html.indexOf(endMarker);
if (start === -1 || end === -1 || end < start) return html;

const contentStart = start + startMarker.length;
return `${html.slice(0, contentStart)}\n${replacement}\n ${html.slice(end)}`;
}

function shareRouteFallback(): string {
return [
' <main class="app-shell" aria-busy="true">',
' <section class="flex flex-col gap-6 pt-8 border-t border-border">',
' <p class="text-sm text-muted" aria-live="polite">Loading share...</p>',
" </section>",
" </main>",
].join("\n");
}

function discoveryRoute(request: Request, url: URL): Response | null {
if (request.method !== "GET" && request.method !== "HEAD") {
return null;
Expand Down Expand Up @@ -308,4 +340,3 @@ export type { AuthUser } from "./auth.ts";
export { cleanTitle, sanitizeShortText, looksLikeHtml, isUploadFile, getClientIp, sha256Hex, hashText, base64Url, numberEnv, formatBytes, escapeHtml, errorMessage, logBackgroundError } from "./utils.ts";
export { maxUploadBytes, appOrigin, previewOrigin } from "./config.ts";
export { createShare, createShareRecord, checkUploadRate, listMyShares, getPublicShare, reportShare, claimShare, deleteShare, listReports, moderateShare, previewShare, previewHeaders, previewMessage } from "./shares.ts";

69 changes: 69 additions & 0 deletions tests/worker-index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import assert from "node:assert/strict";
import { test, vi } from "vitest";

import worker from "../src/worker/index.ts";

const INDEX_HTML = `<!doctype html>
<html lang="en">
<head>
<title>Share HTML</title>
</head>
<body>
<div id="root">
<!-- share-html:fallback:start -->
<main>
<h1>Share HTML — upload one HTML file, get a sandboxed shareable link</h1>
</main>
<!-- share-html:fallback:end -->
</div>
<script type="module" src="/assets/index.js"></script>
</body>
</html>`;

function makeEnv() {
return {
SUPABASE_URL: "https://proj.supabase.co",
SUPABASE_PUBLISHABLE_KEY: "publishable-key",
ASSETS: {
fetch: vi.fn(async () => new Response(INDEX_HTML, {
headers: { "content-type": "text/html" },
})),
},
} as any;
}

function makeCtx() {
return { waitUntil: vi.fn() } as any;
}

test("worker fetch: share SPA fallback does not expose homepage copy before React renders", async () => {
const env = makeEnv();
const response = await worker.fetch(
new Request("https://sharehtml.zhenjia.dev/s/example"),
env,
makeCtx()
);

assert.equal(response.status, 200);
const html = await response.text();

assert.ok(html.includes("Loading share..."));
assert.ok(!html.includes("Share HTML — upload one HTML file"));
assert.ok(html.includes("window.__APP_CONFIG__"));
});

test("worker fetch: homepage SPA fallback keeps agent-readable static copy", async () => {
const env = makeEnv();
const response = await worker.fetch(
new Request("https://sharehtml.zhenjia.dev/"),
env,
makeCtx()
);

assert.equal(response.status, 200);
const html = await response.text();

assert.ok(html.includes("Share HTML — upload one HTML file"));
assert.ok(!html.includes("Loading share..."));
assert.ok(html.includes("window.__APP_CONFIG__"));
});
Loading