p5.jsスクリプトを実行して描画するCloudflare Workers
ベースは以下を参考にさせてもらった
変更点
?public=trueを渡すと、Scrapboxの公開用プロジェクトURLに書き換え
自分のデフォルトScrapboxプロジェクトはprivateで、その一部をpublicに同期させているため
p5.jsのバージョンを更新
以下Workerエンドポイントはダミーだが、利用イメージ
mu373がminami-publicに置き換えて実行される
CORS制限のあるJSにアクセスするために、サーバ側でfetchするようにした
code:worker.js
addEventListener("fetch", (event) => {
event.respondWith(handleRequest(event.request));
});
/**
* Fetch external scripts server-side and inline them into the HTML
* to avoid CORP/CORS blocking (ERR_BLOCKED_BY_RESPONSE.NotSameOrigin).
*/
async function handleRequest(request) {
const url = new URL(request.url);
const params = url.searchParams;
let codelist = params.get("code");
let base64code = params.get("base64code");
let isPublic = params.get("public") === "true";
if (!codelist && !base64code) {
return new Response(
JSON.stringify({
error: "Missing 'code' or 'base64code' parameter",
message:
"Provide script URLs via 'code' parameter or Base64-encoded script via 'base64code'.",
}),
{
status: 400,
headers: { "Content-Type": "application/json; charset=utf-8" },
}
);
}
let inlineScripts = "";
let links = "";
// Optional host allowlist for security
const ALLOW_HOSTS = new Set([
"scrapbox.io",
"raw.githubusercontent.com",
"cdn.jsdelivr.net",
"gist.githubusercontent.com",
]);
if (codelist) {
const codes = codelist.split(",");
for (const code of codes) {
let sanitizedCode = decodeURIComponent(code.trim());
if (!sanitizedCode) continue;
if (isPublic && sanitizedCode.includes("/mu373/")) {
sanitizedCode = sanitizedCode.replace("/mu373/", "/minami-public/");
}
try {
const u = new URL(sanitizedCode);
throw new Error("Only HTTPS URLs are allowed");
}
if (!ALLOW_HOSTS.has(u.host)) {
// Uncomment if you want strict host filtering
// throw new Error(Host not allowed: ${u.host});
}
const res = await fetch(sanitizedCode, {
cf: { cacheTtl: 300, cacheEverything: true },
});
if (!res.ok) throw new Error(Fetch failed: ${res.status});
const jsText = await res.text();
inlineScripts += \n<script>\n${jsText}\n</script>\n;
try {
const after = sanitizedCode.split("/code/")1; if (after) {
const projectDirs = after.split("/");
for (let i = 0; i < projectDirs.length - 1; i++) {
projectURL += "/" + projectDirsi; }
} else {
projectURL = sanitizedCode;
}
} catch {
projectURL = sanitizedCode;
}
links += <a href="${projectURL}" style="margin-right:5px; color:rgba(255,255,255,0.5); text-decoration:none;">[source]</a>;
} catch (err) {
inlineScripts += `\n<script>console.error(${JSON.stringify(
Failed to load ${sanitizedCode}: ${err.message}
)});</script>\n`;
}
}
}
if (base64code) {
try {
const decodedScript = atob(base64code);
inlineScripts += <script>${decodedScript}</script>;
} catch {
return new Response(
JSON.stringify({
error: "Invalid Base64 encoding",
message: "The 'base64code' parameter is not a valid Base64 string.",
}),
{
status: 400,
headers: { "Content-Type": "application/json; charset=utf-8" },
}
);
}
}
const html = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta
name="viewport"
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
<title>Run p5.js</title>
<style>
body { padding:0; margin:0; -webkit-text-size-adjust:100%; }
canvas { display:block; }
position:absolute; z-index:1000; font-size:0.8rem; height:1.5rem;
line-height:1.5rem; text-align:center; color:white;
background:rgba(0,0,0,0.5); margin:0; padding:0 10px; right:0; bottom:0;
font-family:sans-serif;
}
color:rgba(255,255,255,0.5); text-decoration:none;
}
</style>
integrity="sha512-1YMgn4j8cIL91s14ByDGmHtBU6+F8bWOMcF47S0cRO3QNm8SKPNexy4s3OCim9fABUtO++nJMtcpWbINWjMSzQ=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
</head>
<body>
${inlineScripts}
<p id="link">${links}</p>
</body>
</html>`;
const headers = new Headers({
"Content-Type": "text/html; charset=utf-8",
"X-Content-Type-Options": "nosniff",
});
return new Response(html, { status: 200, headers });
}