commit cf2e17f66890986156e6d231964ff3e418915803
parent 82f10323cbc619c506a07f2d473f7ed840954ef4
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Thu, 2 Jul 2026 02:12:19 -0400
Phase 6d: hub-parent indicator + editor pwa/hubUrl fields
Finish Phase 6's hub-parent axis:
- Header "Hub" backlink now resolves via resolveHubUrl(site) (honors a per-site
hubUrl override, not just the family homepageUrl default), renders only in
site mode (a hub never links to itself), and hides when the parent is this
deployment.
- Editor site form gains a "Hub URL" field (validated absolute http(s), else
inherit the family default) and a "Ship as an installable PWA" checkbox
(default off). saveSiteAction reads both and writeSite round-trips them
(serializeSite already persisted pwa/hubUrl). Nothing to add to the settings
form — the family default reuses the existing SiteSettings.homepageUrl.
Site mode stays inert unless a site opts in; profile-row e2e green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat:
3 files changed, 48 insertions(+), 2 deletions(-)
diff --git a/editor/app/sites/actions.ts b/editor/app/sites/actions.ts
@@ -61,6 +61,17 @@ export async function saveSiteAction(
}
const siteUrl = parseSiteUrl(siteUrlRaw);
+ // Hub parent (per-site override of the family default) + PWA opt-in.
+ const hubUrlRaw = String(formData.get("hubUrl") ?? "").trim();
+ if (hubUrlRaw && !parseSiteUrl(hubUrlRaw)) {
+ return {
+ ok: false,
+ error: "Hub URL must be an absolute http(s):// URL, or left blank.",
+ };
+ }
+ const hubUrl = parseSiteUrl(hubUrlRaw);
+ const pwa = formData.get("pwa") === "on";
+
let relatedSitesInput: unknown;
try {
relatedSitesInput = JSON.parse(
@@ -149,6 +160,8 @@ export async function saveSiteAction(
...(parseAccent(accentRaw) ? { accent: parseAccent(accentRaw) } : {}),
...(cloudflareProject ? { cloudflareProject } : {}),
...(siteUrl ? { siteUrl } : {}),
+ ...(hubUrl ? { hubUrl } : {}),
+ ...(pwa ? { pwa: true } : {}),
...(relatedSites.length > 0 ? { relatedSites } : {}),
};
try {
diff --git a/editor/app/sites/components/SiteForm.tsx b/editor/app/sites/components/SiteForm.tsx
@@ -239,6 +239,26 @@ export function SiteForm({ initial, channels, allSites, isNew }: Props) {
defaultValue={initial.siteUrl ?? ""}
hint="Absolute URL this site is served at (e.g. https://jeralyzer.com). Used so other sites can link to it in their footer. Leave blank to omit this site from cross-site lists."
/>
+ <Field
+ label="Hub URL"
+ name="hubUrl"
+ defaultValue={initial.hubUrl ?? ""}
+ hint="The hub this site belongs under (e.g. https://archilyzer.com). Shows a 'Hub' backlink and lets the hub recognize this site as a member. Leave blank to inherit the family default from Settings."
+ />
+ <label className="flex items-center gap-2 text-sm">
+ <input
+ type="checkbox"
+ name="pwa"
+ defaultChecked={initial.pwa === true}
+ className="accent-brand"
+ />
+ Ship as an installable PWA
+ </label>
+ <p className="-mt-2 text-xs text-muted-foreground">
+ Off by default: the site serves its data for the hub to read but isn't
+ independently installable and ships no service worker. Turn on to make
+ this site installable with offline support.
+ </p>
<fieldset className="flex flex-col gap-3 border border-border rounded p-3">
<legend className="px-1 text-sm font-medium">Channels</legend>
diff --git a/export/app/components/Header.tsx b/export/app/components/Header.tsx
@@ -1,10 +1,15 @@
import Link from "next/link";
import { ArrowUpLeft } from "lucide-react";
import { getSettings } from "yt-dlp-transcript-common/lib/settings";
-import { listSites, resolveRelatedSites } from "yt-dlp-transcript-common/lib/site";
+import {
+ listSites,
+ resolveHubUrl,
+ resolveRelatedSites,
+} from "yt-dlp-transcript-common/lib/site";
import { ThemeToggle } from "yt-dlp-transcript-common/components/ThemeToggle";
import { ThemeMenu } from "yt-dlp-transcript-common/components/ThemeMenu";
import { currentSite } from "../lib/site";
+import { instanceMode } from "../lib/mode";
import SiblingSwitcher from "./SiblingSwitcher";
// The export site's masthead: a brand-colored mark + wordmark, calm sans nav,
@@ -15,7 +20,15 @@ import SiblingSwitcher from "./SiblingSwitcher";
export default function Header() {
const site = currentSite();
const settings = getSettings();
- const hubUrl = settings.homepageUrl;
+ // The hub this site points visitors toward: its own hubUrl override, else the
+ // family default (resolveHubUrl). Only a plain site shows the backlink — the
+ // hub itself never links to itself — and never when the parent is this very
+ // deployment.
+ const resolvedHub = resolveHubUrl(site, settings);
+ const hubUrl =
+ instanceMode() === "site" && resolvedHub && resolvedHub !== site.siteUrl
+ ? resolvedHub
+ : undefined;
const related = resolveRelatedSites(site, listSites());
return (