chore(web): post-#363 cleanup — DRY tripwires, drop unused description, singular playlist title
M2 — Add TODO(#375) tripwire comments where the dark/light theme-color hex pair is duplicated outside tokens.json (vite.config.ts and applyMetaThemeColor.svelte.ts). Refactoring is out of scope for #363; the comments are stop signs for the next person who edits these. M5 — The SPA never reads window.__MINSTREL__.description. The OG meta description is server-rendered and complete on its own. Drop the dead inline-script field and the corresponding helpers in branding.ts. Server-side BrandingConfig.Description stays — it's still used for <meta name="description"> and og:description. Copy nit — Playlist detail page title becomes "Playlist · Foo" matching the singular form already used by Artist · / Album ·.
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||||
import { appName, description, pageTitle } from './branding';
|
import { appName, pageTitle } from './branding';
|
||||||
|
|
||||||
const setGlobal = (g: { appName: string; description: string } | undefined) => {
|
const setGlobal = (g: { appName: string } | undefined) => {
|
||||||
if (g === undefined) {
|
if (g === undefined) {
|
||||||
delete (window as { __MINSTREL__?: unknown }).__MINSTREL__;
|
delete (window as { __MINSTREL__?: unknown }).__MINSTREL__;
|
||||||
} else {
|
} else {
|
||||||
@@ -14,7 +14,7 @@ describe('branding helpers', () => {
|
|||||||
afterEach(() => setGlobal(undefined));
|
afterEach(() => setGlobal(undefined));
|
||||||
|
|
||||||
it('appName returns the global value when set', () => {
|
it('appName returns the global value when set', () => {
|
||||||
setGlobal({ appName: 'Family Jukebox', description: 'home' });
|
setGlobal({ appName: 'Family Jukebox' });
|
||||||
expect(appName()).toBe('Family Jukebox');
|
expect(appName()).toBe('Family Jukebox');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -23,31 +23,22 @@ describe('branding helpers', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('appName falls back to "Minstrel" when value contains an unrendered Go token', () => {
|
it('appName falls back to "Minstrel" when value contains an unrendered Go token', () => {
|
||||||
setGlobal({ appName: '{{ .AppName }}', description: 'x' });
|
setGlobal({ appName: '{{ .AppName }}' });
|
||||||
expect(appName()).toBe('Minstrel');
|
expect(appName()).toBe('Minstrel');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('description returns the global value when set', () => {
|
|
||||||
setGlobal({ appName: 'Minstrel', description: 'Our home library.' });
|
|
||||||
expect(description()).toBe('Our home library.');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('description falls back to a default tagline when unset', () => {
|
|
||||||
expect(description()).toContain('Self-hosted music server');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('pageTitle with section returns "{appName} · {section}"', () => {
|
it('pageTitle with section returns "{appName} · {section}"', () => {
|
||||||
setGlobal({ appName: 'Minstrel', description: 'x' });
|
setGlobal({ appName: 'Minstrel' });
|
||||||
expect(pageTitle('Search')).toBe('Minstrel · Search');
|
expect(pageTitle('Search')).toBe('Minstrel · Search');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('pageTitle without section returns just the appName', () => {
|
it('pageTitle without section returns just the appName', () => {
|
||||||
setGlobal({ appName: 'Minstrel', description: 'x' });
|
setGlobal({ appName: 'Minstrel' });
|
||||||
expect(pageTitle()).toBe('Minstrel');
|
expect(pageTitle()).toBe('Minstrel');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('pageTitle composes nested sections via the · separator', () => {
|
it('pageTitle composes nested sections via the · separator', () => {
|
||||||
setGlobal({ appName: 'Minstrel', description: 'x' });
|
setGlobal({ appName: 'Minstrel' });
|
||||||
expect(pageTitle('Library · Artists')).toBe('Minstrel · Library · Artists');
|
expect(pageTitle('Library · Artists')).toBe('Minstrel · Library · Artists');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-10
@@ -1,12 +1,10 @@
|
|||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
__MINSTREL__?: { appName: string; description: string };
|
__MINSTREL__?: { appName: string };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_APP_NAME = 'Minstrel';
|
const DEFAULT_APP_NAME = 'Minstrel';
|
||||||
const DEFAULT_DESCRIPTION =
|
|
||||||
'Self-hosted music server with Subsonic compatibility, smart shuffle, and Lidarr integration.';
|
|
||||||
|
|
||||||
const isRenderedToken = (v: string | undefined): v is string =>
|
const isRenderedToken = (v: string | undefined): v is string =>
|
||||||
typeof v === 'string' && v.length > 0 && !v.includes('{{');
|
typeof v === 'string' && v.length > 0 && !v.includes('{{');
|
||||||
@@ -18,13 +16,6 @@ export const appName = (): string => {
|
|||||||
: DEFAULT_APP_NAME;
|
: DEFAULT_APP_NAME;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const description = (): string => {
|
|
||||||
if (typeof window === 'undefined') return DEFAULT_DESCRIPTION;
|
|
||||||
return isRenderedToken(window.__MINSTREL__?.description)
|
|
||||||
? window.__MINSTREL__!.description
|
|
||||||
: DEFAULT_DESCRIPTION;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const pageTitle = (section?: string): string =>
|
export const pageTitle = (section?: string): string =>
|
||||||
section ? `${appName()} · ${section}` : appName();
|
section ? `${appName()} · ${section}` : appName();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { resolvedTheme } from '$lib/stores/theme.svelte';
|
import { resolvedTheme } from '$lib/stores/theme.svelte';
|
||||||
|
|
||||||
|
// TODO(#375): keep these hex values in sync with tokens.json
|
||||||
|
// colors.dark.obsidian / colors.light.obsidian. The duplication is a known
|
||||||
|
// drift hazard from #363; consume from the generated CSS layer instead.
|
||||||
const DARK = '#14171A';
|
const DARK = '#14171A';
|
||||||
const LIGHT = '#F8F5EE';
|
const LIGHT = '#F8F5EE';
|
||||||
|
|
||||||
|
|||||||
@@ -138,7 +138,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>{pageTitle(playlistQuery?.data?.name ? `Playlists · ${playlistQuery.data.name}` : 'Playlists')}</title>
|
<title>{pageTitle(playlistQuery?.data?.name ? `Playlist · ${playlistQuery.data.name}` : 'Playlist')}</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="mx-auto max-w-4xl px-4 py-6">
|
<div class="mx-auto max-w-4xl px-4 py-6">
|
||||||
|
|||||||
+2
-1
@@ -29,9 +29,10 @@ const injectGoTokensPlugin = {
|
|||||||
<meta property="og:image" content="/brand/og-image.png">
|
<meta property="og:image" content="/brand/og-image.png">
|
||||||
<meta property="og:type" content="website">
|
<meta property="og:type" content="website">
|
||||||
<meta name="twitter:card" content="summary_large_image">
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
<!-- TODO(#375): keep theme-color hex in sync with tokens.json colors.dark.obsidian / colors.light.obsidian -->
|
||||||
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#14171A">
|
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#14171A">
|
||||||
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#F8F5EE">
|
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#F8F5EE">
|
||||||
<script>window.__MINSTREL__ = { appName: "{{ .AppName }}", description: "{{ .Description }}" };</script>`;
|
<script>window.__MINSTREL__ = { appName: "{{ .AppName }}" };</script>`;
|
||||||
|
|
||||||
// Fail loudly if either anchor drifts. A silent .replace() no-op
|
// Fail loudly if either anchor drifts. A silent .replace() no-op
|
||||||
// would ship a build with un-substituted Go template tokens missing,
|
// would ship a build with un-substituted Go template tokens missing,
|
||||||
|
|||||||
Reference in New Issue
Block a user