fix(rsocials): subdomain-aware link generation

Links on subdomain routing (e.g. jeff.rspace.online) were including
the space in the path (/demo/rsocials/campaigns) instead of just
/rsocials/campaigns. Added basePath getter to all components and
detect subdomain in the server-rendered hub page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-09 23:31:45 -07:00
parent eedf2cf189
commit a5c7bb784e
5 changed files with 37 additions and 11 deletions

View File

@ -72,6 +72,14 @@ export class FolkCampaignManager extends HTMLElement {
});
}
private get basePath() {
const host = window.location.hostname;
if (host.endsWith('.rspace.online') || host.endsWith('.rsocials.online')) {
return '/rsocials/';
}
return `/${this._space}/rsocials/`;
}
private esc(s: string): string {
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
@ -131,7 +139,7 @@ export class FolkCampaignManager extends HTMLElement {
</div>
</div>
<div class="actions">
<a href="/rsocials/thread-editor" class="btn btn--outline">Open Thread Editor</a>
<a href="${this.basePath}thread-editor" class="btn btn--outline">Open Thread Editor</a>
<button class="btn btn--primary" id="import-md-btn">Import from Markdown</button>
</div>
${phaseHTML}

View File

@ -121,6 +121,14 @@ class FolkCampaignPlanner extends HTMLElement {
private shadow: ShadowRoot;
private space = '';
private get basePath() {
const host = window.location.hostname;
if (host.endsWith('.rspace.online') || host.endsWith('.rsocials.online')) {
return '/rsocials/';
}
return `/${this.space}/rsocials/`;
}
// Data
private nodes: CampaignPlannerNode[] = [];
private edges: CampaignEdge[] = [];
@ -820,7 +828,7 @@ class FolkCampaignPlanner extends HTMLElement {
} else if (action === 'open-thread') {
const d = node.data as ThreadNodeData;
if (d.threadId) {
window.location.href = `/rsocials/thread-editor/${d.threadId}/edit`;
window.location.href = `${this.basePath}thread-editor/${d.threadId}/edit`;
}
}
});
@ -1457,7 +1465,7 @@ class FolkCampaignPlanner extends HTMLElement {
if (node?.type === 'thread') {
const d = node.data as ThreadNodeData;
if (d.threadId) {
window.location.href = `/rsocials/thread-editor/${d.threadId}/edit`;
window.location.href = `${this.basePath}thread-editor/${d.threadId}/edit`;
}
}
});

View File

@ -274,7 +274,7 @@ export class FolkThreadBuilder extends HTMLElement {
</div>
<div class="preview ro-cards">${tweetCards}</div>
<div class="ro-actions">
<a href="/rsocials/thread-editor/${this.esc(t.id)}/edit" class="btn btn--primary">Edit Thread</a>
<a href="${this.basePath}thread-editor/${this.esc(t.id)}/edit" class="btn btn--primary">Edit Thread</a>
<button class="btn btn--outline" id="ro-copy-thread">Copy Thread</button>
<button class="btn btn--outline" id="ro-copy-link">Copy Link</button>
<div class="export-dropdown">
@ -289,8 +289,8 @@ export class FolkThreadBuilder extends HTMLElement {
</div>
</div>
<div class="ro-cta">
<a href="/rsocials/thread-editor" class="btn btn--success">Create Your Own Thread</a>
<a href="/rsocials/threads" class="btn btn--outline">Browse All Threads</a>
<a href="${this.basePath}thread-editor" class="btn btn--success">Create Your Own Thread</a>
<a href="${this.basePath}threads" class="btn btn--outline">Browse All Threads</a>
</div>
</div>
<div class="toast" id="export-toast" hidden></div>

View File

@ -82,6 +82,14 @@ export class FolkThreadGallery extends HTMLElement {
this.render();
}
private get basePath() {
const host = window.location.hostname;
if (host.endsWith('.rspace.online') || host.endsWith('.rsocials.online')) {
return '/rsocials/';
}
return `/${this._space}/rsocials/`;
}
private esc(s: string): string {
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
@ -94,7 +102,7 @@ export class FolkThreadGallery extends HTMLElement {
const cardsHTML = threads.length === 0
? `<div class="empty">
<p>No threads yet. Create your first thread!</p>
<a href="/rsocials/thread-editor" class="btn btn--success">Create Thread</a>
<a href="${this.basePath}thread-editor" class="btn btn--success">Create Thread</a>
</div>`
: `<div class="grid">
${threads.map(t => {
@ -105,8 +113,8 @@ export class FolkThreadGallery extends HTMLElement {
? `<div class="card__image"><img src="${this.esc(t.imageUrl)}" alt="" loading="lazy"></div>`
: '';
const href = this._isDemoFallback
? `/rsocials/thread-editor`
: `/rsocials/thread-editor/${this.esc(t.id)}/edit`;
? `${this.basePath}thread-editor`
: `${this.basePath}thread-editor/${this.esc(t.id)}/edit`;
return `<a href="${href}" class="card">
${imageTag}
<h3 class="card__title">${this.esc(t.title || 'Untitled Thread')}</h3>
@ -165,7 +173,7 @@ export class FolkThreadGallery extends HTMLElement {
<div class="gallery">
<div class="header">
<h1>Threads</h1>
<a href="/rsocials/thread-editor" class="btn btn--primary">New Thread</a>
<a href="${this.basePath}thread-editor" class="btn btn--primary">New Thread</a>
</div>
${cardsHTML}
</div>

View File

@ -644,7 +644,9 @@ routes.get("/landing", (c) => {
routes.get("/", (c) => {
const space = c.req.param("space") || "demo";
const base = `/${escapeHtml(space)}/rsocials`;
const host = c.req.header("host")?.split(":")[0] || "";
const isSubdomain = (host.endsWith(".rspace.online") && host !== "rspace.online" && !host.startsWith("www.")) || host.endsWith(".rsocials.online");
const base = isSubdomain ? "/rsocials" : `/${escapeHtml(space)}/rsocials`;
return c.html(renderShell({
title: `rSocials — ${space} | rSpace`,
moduleId: "rsocials",