fix: replace broken Postiz reverse proxy with iframe embed

The reverse proxy couldn't work for a Next.js SPA — it caused double
space prefix in redirects and couldn't handle /_next/* static assets.
Switch to iframe approach with demo.rsocials.online directly. Fix
template literal bugs where getSchedulerUrl() was passed as a string
literal instead of being evaluated.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-05 10:20:19 -08:00
parent bb37727f18
commit 493ae2442c
2 changed files with 18 additions and 80 deletions

View File

@ -164,7 +164,7 @@ class FolkSocialsCanvas extends HTMLElement {
} }
private getSchedulerUrl(): string { private getSchedulerUrl(): string {
return `${this.getApiBase()}/scheduler`; return "https://demo.rsocials.online";
} }
private buildCanvasNodes() { private buildCanvasNodes() {
@ -407,7 +407,7 @@ class FolkSocialsCanvas extends HTMLElement {
<span class="sc-postiz-title">Postiz Post Scheduler</span> <span class="sc-postiz-title">Postiz Post Scheduler</span>
<button class="sc-postiz-close" id="close-postiz">\u2715</button> <button class="sc-postiz-close" id="close-postiz">\u2715</button>
</div> </div>
<iframe class="sc-postiz-iframe" src="${this.postizOpen ? "${this.getSchedulerUrl()}" : "about:blank"}" title="Postiz"></iframe> <iframe class="sc-postiz-iframe" src="${this.postizOpen ? this.getSchedulerUrl() : "about:blank"}" title="Postiz"></iframe>
</div> </div>
</div> </div>
</div> </div>
@ -423,7 +423,7 @@ class FolkSocialsCanvas extends HTMLElement {
const panel = this.shadow.getElementById("postiz-panel"); const panel = this.shadow.getElementById("postiz-panel");
const iframe = panel?.querySelector("iframe"); const iframe = panel?.querySelector("iframe");
if (panel) panel.classList.toggle("open", this.postizOpen); if (panel) panel.classList.toggle("open", this.postizOpen);
if (iframe && this.postizOpen) iframe.src = "${this.getSchedulerUrl()}"; if (iframe && this.postizOpen) iframe.src = this.getSchedulerUrl();
if (iframe && !this.postizOpen) iframe.src = "about:blank"; if (iframe && !this.postizOpen) iframe.src = "about:blank";
}); });
this.shadow.getElementById("close-postiz")?.addEventListener("click", () => { this.shadow.getElementById("close-postiz")?.addEventListener("click", () => {

View File

@ -514,84 +514,22 @@ function renderDemoFeedHTML(): string {
// ── Path-based sub-routes ── // ── Path-based sub-routes ──
// ── Postiz reverse proxy at /scheduler/* ── // ── Postiz scheduler — embedded via iframe ──
// Proxies to the Postiz container on the Docker network, avoiding // Postiz runs at demo.rsocials.online (Traefik priority 130 > rSpace's 120).
// the Traefik priority conflict with rSpace's rsocials.online catch-all. // The /scheduler route renders a full-page iframe shell.
const POSTIZ_UPSTREAM = process.env.POSTIZ_URL || "http://postiz:5000"; const POSTIZ_URL = process.env.POSTIZ_URL || "https://demo.rsocials.online";
routes.all("/scheduler/*", async (c) => { routes.get("/scheduler", (c) => {
const subPath = c.req.path.replace(/^\/[^/]+\/rsocials\/scheduler/, "") || "/";
const upstreamUrl = `${POSTIZ_UPSTREAM}${subPath}`;
const url = new URL(upstreamUrl);
// Preserve query string
const reqUrl = new URL(c.req.url);
url.search = reqUrl.search;
try {
const headers = new Headers(c.req.raw.headers);
headers.delete("host");
const resp = await fetch(url.toString(), {
method: c.req.method,
headers,
body: c.req.method !== "GET" && c.req.method !== "HEAD" ? c.req.raw.body : undefined,
redirect: "manual",
});
// Rewrite Location headers to stay within /scheduler/
const respHeaders = new Headers(resp.headers);
const location = respHeaders.get("location");
if (location) {
try {
const locUrl = new URL(location, upstreamUrl);
if (locUrl.origin === new URL(POSTIZ_UPSTREAM).origin) {
const space = c.req.param("space") || "demo"; const space = c.req.param("space") || "demo";
respHeaders.set("location", `/${space}/rsocials/scheduler${locUrl.pathname}${locUrl.search}`); return c.html(renderExternalAppShell({
} title: `Post Scheduler — rSocials | rSpace`,
} catch { /* keep original */ } moduleId: "rsocials",
} spaceSlug: space,
modules: getModuleInfoList(),
return new Response(resp.body, { theme: "dark",
status: resp.status, appName: "Postiz",
headers: respHeaders, appUrl: POSTIZ_URL,
}); }));
} catch {
return c.text("Postiz scheduler unavailable", 502);
}
});
routes.get("/scheduler", async (c) => {
const space = c.req.param("space") || "demo";
const upstreamUrl = `${POSTIZ_UPSTREAM}/`;
try {
const headers = new Headers(c.req.raw.headers);
headers.delete("host");
const reqUrl = new URL(c.req.url);
const resp = await fetch(`${upstreamUrl}${reqUrl.search}`, {
method: "GET",
headers,
redirect: "manual",
});
const respHeaders = new Headers(resp.headers);
const location = respHeaders.get("location");
if (location) {
try {
const locUrl = new URL(location, upstreamUrl);
if (locUrl.origin === new URL(POSTIZ_UPSTREAM).origin) {
respHeaders.set("location", `/${space}/rsocials/scheduler${locUrl.pathname}${locUrl.search}`);
}
} catch { /* keep original */ }
}
return new Response(resp.body, {
status: resp.status,
headers: respHeaders,
});
} catch {
return c.text("Postiz scheduler unavailable", 502);
}
}); });
routes.get("/feed", (c) => { routes.get("/feed", (c) => {
@ -660,7 +598,7 @@ export const socialsModule: RSpaceModule = {
// Run migration for any existing file-based threads // Run migration for any existing file-based threads
try { await migrateFileThreadsToAutomerge("demo"); } catch { /* ignore */ } try { await migrateFileThreadsToAutomerge("demo"); } catch { /* ignore */ }
}, },
externalApp: { url: "https://demo.rsocials.online", name: "Postiz" }, externalApp: { url: POSTIZ_URL, name: "Postiz" },
feeds: [ feeds: [
{ {
id: "social-feed", id: "social-feed",