commit 3dbc29034b3ae782ff432963925e35e8bfde1003
parent 3d273ea14b4d564542222c05c3d327548dfef577
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Mon, 27 Apr 2026 23:23:53 -0400
feat(editor): use site/header title from settings.json for editor layout
Layout now reads siteTitle for the document title and headerTitle (with
siteTitle fallback) for the sidebar brand, with a small "editor" sub-label
to keep editor vs export distinguishable. Drops the in-memory settings cache
so file-level edits (cypress fixtures, manual writes) are picked up on the
next request without a server restart.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diffstat:
3 files changed, 66 insertions(+), 13 deletions(-)
diff --git a/common/lib/settings.ts b/common/lib/settings.ts
@@ -21,10 +21,7 @@ const DEFAULTS: SiteSettings = {
maxTranscriptPageBytes: TRANSCRIPT_PAGE_DEFAULT_BYTES,
};
-let cached: SiteSettings | null = null;
-
export function getSettings(): SiteSettings {
- if (cached) return cached;
const file = getPaths().settingsFile;
let parsed: Partial<SiteSettings> = {};
try {
@@ -34,8 +31,7 @@ export function getSettings(): SiteSettings {
}
const merged = { ...DEFAULTS, ...parsed };
merged.maxTranscriptPageBytes = clampPageBytes(merged.maxTranscriptPageBytes);
- cached = merged;
- return cached;
+ return merged;
}
function clampPageBytes(value: unknown): number {
@@ -57,5 +53,4 @@ export async function writeSettings(next: SiteSettings): Promise<void> {
const tmp = `${file}.tmp-${process.pid}`;
await fs.promises.writeFile(tmp, JSON.stringify(merged, null, 2) + "\n");
await fs.promises.rename(tmp, file);
- cached = null;
}
diff --git a/editor/app/layout.tsx b/editor/app/layout.tsx
@@ -1,11 +1,16 @@
import type { Metadata } from "next";
import Link from "next/link";
+import { getSettings } from "yt-dlp-transcript-common/lib/settings";
import "./globals.css";
-export const metadata: Metadata = {
- title: "Transcript Editor",
- description: "Editor for yt-dlp-transcript-browser",
-};
+export async function generateMetadata(): Promise<Metadata> {
+ const settings = getSettings();
+ return {
+ title: settings.siteTitle,
+ description:
+ settings.siteDescription || "Editor for yt-dlp-transcript-browser",
+ };
+}
const NAV_LINKS = [
{ href: "/", label: "Dashboard" },
@@ -15,19 +20,28 @@ const NAV_LINKS = [
{ href: "/settings", label: "Settings" },
];
-export default function RootLayout({
+export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
+ const settings = getSettings();
+ const headerLabel = settings.headerTitle || settings.siteTitle;
return (
<html lang="en" className="h-full antialiased">
<body className="min-h-full flex bg-zinc-50 text-zinc-900 dark:bg-zinc-950 dark:text-zinc-100">
<aside className="w-56 shrink-0 border-r border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 flex flex-col">
<div className="px-4 py-4 border-b border-zinc-200 dark:border-zinc-800">
- <Link href="/" className="font-semibold tracking-tight">
- Editor
+ <Link
+ href="/"
+ data-testid="sidebar-brand"
+ className="font-semibold tracking-tight block leading-tight"
+ >
+ {headerLabel}
</Link>
+ <div className="text-xs uppercase tracking-wide text-zinc-500 mt-0.5">
+ editor
+ </div>
</div>
<nav className="flex flex-col p-2 gap-1 text-sm">
{NAV_LINKS.map((link) => (
diff --git a/editor/cypress/e2e/branding.cy.ts b/editor/cypress/e2e/branding.cy.ts
@@ -0,0 +1,44 @@
+describe("Editor branding from settings.json", () => {
+ it("renders siteTitle in document title and headerTitle in sidebar", () => {
+ cy.resetData("empty");
+ cy.writeSettings({
+ siteTitle: "Cypress Browser",
+ siteDescription: "x",
+ headerTitle: "Cypress HQ",
+ homeTagline: "",
+ maxTranscriptPageBytes: 8388608,
+ });
+ // Re-invalidate the in-memory settings cache after the file was rewritten.
+ cy.request({ url: "/api/test/invalidate-cache", failOnStatusCode: false });
+
+ cy.visit("/");
+ cy.title().should("include", "Cypress Browser");
+ cy.findByTestId("sidebar-brand").should("contain.text", "Cypress HQ");
+ });
+
+ it("falls back to siteTitle when headerTitle is empty", () => {
+ cy.resetData("empty");
+ cy.writeSettings({
+ siteTitle: "Just A Title",
+ siteDescription: "",
+ headerTitle: "",
+ homeTagline: "",
+ maxTranscriptPageBytes: 8388608,
+ });
+ cy.request({ url: "/api/test/invalidate-cache", failOnStatusCode: false });
+
+ cy.visit("/");
+ cy.findByTestId("sidebar-brand").should("contain.text", "Just A Title");
+ });
+
+ it("reflects sidebar header after saving via the settings form", () => {
+ cy.resetData("empty");
+ cy.visit("/settings");
+ cy.findByLabelText(/header title/i).clear().type("Saved Header");
+ cy.findByRole("button", { name: /save settings/i }).click();
+ cy.findByTestId("settings-saved", { timeout: 10_000 });
+
+ cy.visit("/");
+ cy.findByTestId("sidebar-brand").should("contain.text", "Saved Header");
+ });
+});