refactor: site metadata

This commit is contained in:
Jason Rasmussen
2026-05-06 16:54:30 -04:00
parent 4369fdffbb
commit 5c3898ec41
30 changed files with 147 additions and 105 deletions
+1
View File
@@ -4,6 +4,7 @@ pnpm-lock.yaml
yarn.lock
src/lib/assets
dist/
deployment/
CHANGELOG.md
.svelte-kit/
.release-please-manifest.json
@@ -13,6 +13,7 @@
ControlBarHeader,
ControlBarOverflow,
getSiteProviders,
globalManager,
Logo,
ThemeSwitcher,
TooltipProvider,
@@ -26,6 +27,7 @@
const { children }: Props = $props();
globalManager.setSiteMetadata(siteMetadata);
commandPaletteManager.enable();
</script>
@@ -27,7 +27,7 @@
let filteredCategories = $derived(filterProjects(query));
</script>
<SiteMetadata site={siteMetadata} />
<SiteMetadata />
<Heading size="giant">{siteMetadata.title}</Heading>
<Text size="large" color="muted">{siteMetadata.description}</Text>
@@ -1,5 +1,4 @@
<script lang="ts">
import { siteMetadata } from '$lib';
import { Card, CardBody, Container, Heading, SiteMetadata, Text } from '@immich/ui';
import { DateTime } from 'luxon';
import type { Snippet } from 'svelte';
@@ -22,7 +21,7 @@
});
</script>
<SiteMetadata site={siteMetadata} page={pageMetadata} />
<SiteMetadata page={pageMetadata} />
<Container size="medium" center>
<section class="flex flex-col gap-2">
-7
View File
@@ -4,13 +4,6 @@ import { defaultProvider, type ActionItem } from '@immich/ui';
import fm from 'front-matter';
import { DateTime } from 'luxon';
export const siteMetadata = {
title: 'Immich',
description:
'Self-hosted photo and video management solution. Easily back up, organize, and manage your photos on your own server. Immich helps you browse, search and organize your photos and videos with ease, without sacrificing your privacy.',
imageUrl: '/img/social-preview.png',
};
export const blogMetadata = {
title: 'Immich Blog',
description: 'Latest updates, announcements, and stories from the Immich team.',
@@ -15,6 +15,7 @@
CommandPaletteProvider,
Constants,
getSiteProviders,
globalManager,
IconButton,
Link,
Logo,
@@ -77,6 +78,12 @@
return active ? 'primary' : 'secondary';
};
globalManager.setSiteMetadata({
title: 'Immich',
description:
'Self-hosted photo and video management solution. Easily back up, organize, and manage your photos on your own server. Immich helps you browse, search and organize your photos and videos with ease, without sacrificing your privacy.',
imageUrl: '/img/social-preview.png',
});
commandPaletteManager.enable();
</script>
+1 -3
View File
@@ -4,7 +4,6 @@
import qrCodeLight from '$common/img/app-qr-code-light.svg';
import screenshotDark from '$common/img/screenshot-dark.webp';
import screenshotLight from '$common/img/screenshot-light.webp';
import { siteMetadata } from '$lib';
import {
appStoreBadge,
Button,
@@ -30,8 +29,7 @@
];
</script>
<!-- only SSR SiteMetadata -->
<SiteMetadata site={siteMetadata} />
<SiteMetadata />
{#if browser}
<VStack gap={8} class="mt-4 text-center lg:mt-16">
@@ -1,6 +1,6 @@
<script lang="ts">
import type { TimelineItem } from '$lib';
import { posts, siteMetadata } from '$lib';
import { posts } from '$lib';
import Timeline from '$lib/components/Timeline.svelte';
import { asGithubLink, Heading, SiteMetadata, Stack, Text } from '@immich/ui';
import {
@@ -234,7 +234,7 @@
};
</script>
<SiteMetadata site={siteMetadata} page={pageMetadata} />
<SiteMetadata page={pageMetadata} />
<Stack class="text-center" gap={4}>
<Heading size="title" tag="h1">{pageMetadata.title}</Heading>
@@ -2,7 +2,6 @@
import portainerIcon from '$common/img/portainer.svg';
import truenasIcon from '$common/img/truenas-scale.svg';
import unraidIcon from '$common/img/unraid.svg';
import { siteMetadata } from '$lib';
import {
appStoreBadge,
Button,
@@ -30,7 +29,7 @@
};
</script>
<SiteMetadata site={siteMetadata} page={pageMetadata} />
<SiteMetadata page={pageMetadata} />
<Heading size="title" class="mb-4" tag="h1">{pageMetadata.title}</Heading>
@@ -1,5 +1,4 @@
<script lang="ts">
import { siteMetadata } from '$lib';
import {
Card,
CardBody,
@@ -256,7 +255,7 @@
};
</script>
<SiteMetadata site={siteMetadata} page={pageMetadata} />
<SiteMetadata page={pageMetadata} />
<Stack gap={8}>
<Stack>
@@ -1,6 +1,5 @@
<script lang="ts">
import type { TimelineItem } from '$lib';
import { siteMetadata } from '$lib';
import Timeline from '$lib/components/Timeline.svelte';
import { Heading, SiteMetadata, Stack, Text } from '@immich/ui';
import {
@@ -947,7 +946,7 @@
};
</script>
<SiteMetadata site={siteMetadata} page={pageMetadata} />
<SiteMetadata page={pageMetadata} />
<Stack class="text-center" gap={4}>
<Heading size="title" tag="h1">Roadmap</Heading>
@@ -1,16 +1,16 @@
<script lang="ts">
import MarkdownPage from '$lib/components/MarkdownPage.svelte';
import { type ContainerSize } from '@immich/ui';
import { Markdown, type ContainerSize } from '@immich/ui';
import type { Snippet } from 'svelte';
type Props = {
name: string;
description: string;
descriptionBody?: Snippet;
size?: ContainerSize;
children?: Snippet;
};
const { size, name, description, children }: Props = $props();
const { size, name, description, descriptionBody, children }: Props = $props();
</script>
<MarkdownPage attributes={{ title: name, description }} {size} {children} />
<Markdown.Page attributes={{ title: name, description }} description={descriptionBody} {size} {children} />
@@ -1,31 +0,0 @@
<script lang="ts">
import PageDescription from '$lib/components/PageDescription.svelte';
import { siteMetadata } from '$lib/constants.js';
import { Container, Heading, SiteMetadata, type BreadcrumbItem, type ContainerSize } from '@immich/ui';
import type { Snippet } from 'svelte';
type Props = {
attributes: { title: string; description: string };
size?: ContainerSize;
breadcrumbs?: BreadcrumbItem[];
children?: Snippet;
};
const { attributes, size = 'medium', children }: Props = $props();
const { title, description } = $derived(attributes);
const page = $derived({ title, description });
</script>
<SiteMetadata site={siteMetadata} {page} />
<Container {size} center class="my-4 flex flex-col p-4 lg:my-8">
<section class="mb-8">
<Heading tag="h1" size="giant">{page.title}</Heading>
{#if page.description}
<PageDescription>{page.description}</PageDescription>
{/if}
</section>
{@render children?.()}
</Container>
@@ -1,12 +0,0 @@
<script lang="ts">
import { Text } from '@immich/ui';
import type { Snippet } from 'svelte';
type Props = {
children: Snippet;
};
const { children }: Props = $props();
</script>
<Text color="muted" size="large" class="mt-2" {children} />
-5
View File
@@ -106,11 +106,6 @@ export type ExampleItem = {
export type ExampleCardProps = ExampleItem;
export const siteMetadata = {
title: 'Immich UI',
description: 'A Svelte component library for Immich',
};
export type ComponentItem = {
name: string;
title?: string;
@@ -1,14 +1,14 @@
<script lang="ts">
import ComponentCard from '$lib/components/ComponentCard.svelte';
import Grid from '$lib/components/Grid.svelte';
import MarkdownPage from '$lib/components/MarkdownPage.svelte';
import { components } from '$lib/constants.js';
import { Markdown } from '@immich/ui';
</script>
<MarkdownPage attributes={{ title: 'Components', description: 'A list of all the Immich UI components' }} size="large">
<Markdown.Page attributes={{ title: 'Components', description: 'A list of all the Immich UI components' }} size="large">
<Grid>
{#each components as component (component.name)}
<ComponentCard {component} />
{/each}
</Grid>
</MarkdownPage>
</Markdown.Page>
@@ -2,7 +2,6 @@
import ComponentExamples from '$lib/components/ComponentExamples.svelte';
import ComponentLink from '$lib/components/ComponentLink.svelte';
import ComponentPage from '$lib/components/ComponentPage.svelte';
import PageDescription from '$lib/components/PageDescription.svelte';
import BasicExample from './BasicExample.svelte';
import basicExample from './BasicExample.svelte?raw';
import ColorExample from './ColorExample.svelte';
@@ -22,7 +21,9 @@
</script>
<ComponentPage name="IconButton" description="An icon-only version of the Button component">
<PageDescription>An icon-only version of the <ComponentLink name="Button" /> component</PageDescription>
{#snippet descriptionBody()}
An icon-only version of the <ComponentLink name="Button" /> component
{/snippet}
<ComponentExamples
examples={[
{ title: 'Basic', code: basicExample, component: BasicExample },
@@ -1,9 +1,8 @@
<script lang="ts">
import ComponentExamples from '$lib/components/ComponentExamples.svelte';
import ComponentPage from '$lib/components/ComponentPage.svelte';
import PageDescription from '$lib/components/PageDescription.svelte';
import { asComponentHref } from '$lib/utilities.js';
import { Link } from '@immich/ui';
import { Link, Text } from '@immich/ui';
import BasicExample from './BasicExample.svelte';
import basicExample from './BasicExample.svelte?raw';
import ExpandExample from './ExpandExample.svelte';
@@ -15,10 +14,10 @@
</script>
<ComponentPage name="Navbar" description="A component for creating navigation items in a sidebar">
<PageDescription
>This component exports a NavbarItem for creating navigation menus. This is usually used in conjunction with the <Link
<Text>
This component exports a NavbarItem for creating navigation menus. This is usually used in conjunction with the <Link
href={asComponentHref('AppShell')}>AppSidebar</Link
> component</PageDescription
> component</Text
>
<ComponentExamples
examples={[
@@ -1,15 +1,13 @@
<script lang="ts">
import ComponentExamples from '$lib/components/ComponentExamples.svelte';
import ComponentPage from '$lib/components/ComponentPage.svelte';
import PageDescription from '$lib/components/PageDescription.svelte';
import BasicExample from './BasicExample.svelte';
import basicExample from './BasicExample.svelte?raw';
</script>
<ComponentPage name="Tooltip" description="A component for displaying tooltips on hover or focus">
<PageDescription>
Displays supplementary information when users hover over or interact with an element.
</PageDescription>
<ComponentPage
name="Tooltip"
description="Display supplementary information when users hover over or interact with an element"
>
<ComponentExamples examples={[{ title: 'Basic', code: basicExample, component: BasicExample }]} />
</ComponentPage>
+6 -1
View File
@@ -1,20 +1,25 @@
<script lang="ts">
import '$lib/app.css';
import { getDocsProviders } from '$lib/constants.js';
import {
commandPaletteManager,
CommandPaletteProvider,
getSiteProviders,
globalManager,
ScreencastOverlay,
toastManager,
TooltipProvider,
} from '@immich/ui';
import '@immich/ui/theme/default.css';
import '$lib/app.css';
let { children } = $props();
toastManager.setOptions({ class: 'top-[58px]' });
globalManager.setSiteMetadata({
title: 'Immich UI',
description: 'A Svelte component library for Immich',
});
commandPaletteManager.enable();
commandPaletteManager.setTranslations({
command_palette_prompt_default: 'Quickly find components, links, and commands',
+1 -2
View File
@@ -1,6 +1,5 @@
<script lang="ts">
import imageUrl from '$lib/assets/design_placeholder.svg';
import { siteMetadata } from '$lib/constants.js';
import {
AppShell,
AppShellHeader,
@@ -23,7 +22,7 @@
import { mdiCodeBraces, mdiOpenInNew } from '@mdi/js';
</script>
<SiteMetadata site={siteMetadata} />
<SiteMetadata />
<AppShell>
<AppShellHeader class="w-full">
+1 -1
View File
@@ -11,7 +11,7 @@ const config = {
preprocess: [
svelteMarkdownPreprocess({
layouts: {
default: '$lib/components/MarkdownPage.svelte',
default: { name: 'Markdown.Page', source: '@immich/ui', import: false },
},
}),
vitePreprocess(),
+2 -1
View File
@@ -72,10 +72,11 @@ export default defineConfig([
'apps/**/backend/dist',
'apps/**/.wrangler',
'apps/**/backend/worker-configuration.d.ts',
'common/',
'deployment/**',
'packages/**/build/',
'packages/**/.svelte-kit/',
'packages/**/dist/',
'common/',
],
},
]);
@@ -9,7 +9,7 @@ export type FileWithContent = { filename: string; content: string };
export type FileWithFrontMatter = { filename: string; attributes: FrontMatterAttributes; body: string };
export type FileWithScriptBody = FileWithFrontMatter & { scriptBody: string };
export type FileWithMarkup = FileWithScriptBody & { markup: string };
export type FileWithLayout = FileWithMarkup & { layout?: string };
export type FileWithLayout = FileWithMarkup & { layout?: SvelteMarkdownImport };
export type FileWithSvelte = FileWithLayout & { svelte: string };
export type FrontMatterAttributes = {
@@ -17,10 +17,14 @@ export type FrontMatterAttributes = {
[key: string]: unknown;
};
export type SvelteMarkdownLayoutHandler = (file: FileWithMarkup) => SvelteMarkdownImportLike;
export type SvelteMarkdownImportLike = SvelteMarkdownImport | string;
export type SvelteMarkdownImport = { name: string; import?: string | false; source: string };
export type SvelteMarkdownPreprocessLayouts = {
_?: string;
default?: string;
[key: string]: string | undefined;
_?: SvelteMarkdownImportLike | SvelteMarkdownLayoutHandler;
default?: SvelteMarkdownImportLike | SvelteMarkdownLayoutHandler;
[key: string]: SvelteMarkdownImportLike | SvelteMarkdownLayoutHandler | undefined;
};
const SCRIPT_BODY_REGEX = /<script.*>(?<body>(.|\n)*?)<\/script>/;
@@ -97,7 +101,21 @@ export class SvelteMarkdownPreprocess {
parseLayout(file: FileWithMarkup): MaybePromise<FileWithLayout> {
const layoutKey = file.attributes.layout;
const layout = layoutKey ? this.#layouts[layoutKey] : (this.#layouts.default ?? this.#layouts._);
let layout = layoutKey ? this.#layouts[layoutKey] : (this.#layouts.default ?? this.#layouts._);
if (layoutKey && !layout) {
throw new Error(`File references unknown layout key: ${file.filename} => ${layoutKey} => ?`);
}
if (typeof layout === 'function') {
layout = layout(file);
}
if (typeof layout === 'string') {
layout = { name: 'Layout', source: layout };
}
return { ...file, layout };
}
@@ -108,10 +126,12 @@ export class SvelteMarkdownPreprocess {
return { ...file, svelte };
}
scriptImports(file: FileWithLayout): Array<string | undefined> {
scriptImports({ layout }: FileWithLayout): Array<string | undefined> {
return [
` import { Markdown } from '@immich/ui';`,
file.layout ? ` import Layout from '${file.layout}';` : undefined,
layout && layout.import !== false
? ` import ${layout.import ?? layout.name} from '${layout.source}';`
: undefined,
];
}
@@ -132,7 +152,9 @@ export class SvelteMarkdownPreprocess {
}
createSvelteTemplate(file: FileWithLayout): string {
return (file.layout ? [`<Layout {attributes}>`, file.markup, '</Layout>'] : [file.markup]).join('\n');
return (
file.layout ? [`<${file.layout.name} {attributes}>`, file.markup, `</${file.layout.name}>`] : [file.markup]
).join('\n');
}
}
@@ -0,0 +1,45 @@
<script lang="ts">
import Container from '$lib/components/Container/Container.svelte';
import Heading from '$lib/components/Heading/Heading.svelte';
import SiteMetadata from '$lib/components/SiteMetadata/SiteMetadata.svelte';
import Text from '$lib/components/Text/Text.svelte';
import { type BreadcrumbItem, type ContainerSize } from '$lib/types.js';
import type { Metadata } from '$lib/utilities/common.js';
import type { Snippet } from 'svelte';
type Props = {
attributes: Metadata;
size?: ContainerSize;
breadcrumbs?: BreadcrumbItem[];
title?: Snippet;
description?: Snippet;
children?: Snippet;
};
const { attributes: page, title, description, size = 'medium', children }: Props = $props();
</script>
<SiteMetadata {page} />
<Container {size} center class="my-4 flex flex-col p-4 lg:my-8">
<section class="mb-8">
<Heading tag="h1" size="giant">
{#if title}
{@render title()}
{:else}
{page.title}
{/if}
</Heading>
{#if page.description || description}
<Text color="muted" size="large" fontWeight="semi-bold" class="mt-2">
{#if typeof description === 'function'}
{@render description()}
{:else}
{page.description}
{/if}
</Text>
{/if}
</section>
{@render children?.()}
</Container>
@@ -7,6 +7,7 @@ import LineBreak from './LineBreak.svelte';
import Link from './Link.svelte';
import List from './List.svelte';
import ListItem from './ListItem.svelte';
import Page from './Page.svelte';
import Paragraph from './Paragraph.svelte';
import Space from './Space.svelte';
import Table from './Table.svelte';
@@ -27,6 +28,7 @@ export const Markdown = {
Image,
ListItem,
List,
Page,
Paragraph,
Table,
TableHeader,
@@ -63,7 +63,7 @@
<button
type="button"
aria-label={expanded ? t('collapse') : t('expand')}
class="hover:bg-subtle hover:text-primary absolute me-2 hidden h-full rounded-lg px-0.5 md:block"
class="hover:bg-subtle hover:text-primary absolute me-2 h-full rounded-lg px-0.5 md:block"
onclick={() => (expanded = !expanded)}
>
<Icon
@@ -1,15 +1,16 @@
<script lang="ts">
import { globalManager } from '$lib/services/global-manager.svelte.js';
import { resolveMetadata, resolveUrl, type ArticleMetadata, type Metadata } from '$lib/utilities/common.js';
type Props = {
site: Metadata;
site?: Metadata;
page?: Metadata;
article?: ArticleMetadata;
};
const { site, page, article }: Props = $props();
let resolved = $derived(resolveMetadata(site, page, article));
let resolved = $derived(resolveMetadata(site ?? globalManager.siteMetadata, page, article));
</script>
<svelte:head>
+1
View File
@@ -109,6 +109,7 @@ export { default as TooltipProvider } from '$lib/components/Tooltip/TooltipProvi
export * from '$lib/actions/shortcut.js';
export * from '$lib/commands.js';
export * from '$lib/services/command-palette-manager.svelte.js';
export * from '$lib/services/global-manager.svelte.js';
export * from '$lib/services/menu-manager.svelte.js';
export * from '$lib/services/modal-manager.svelte.js';
export * from '$lib/services/screencast-manager.svelte.js';
@@ -0,0 +1,19 @@
import { type Metadata } from '$lib/utilities/common.js';
class GlobalManager {
#siteMetadata = $state<Metadata>();
get siteMetadata() {
if (!this.#siteMetadata) {
throw new Error('No site metadata!');
}
return this.#siteMetadata;
}
setSiteMetadata(site: Metadata) {
this.#siteMetadata = site;
}
}
export const globalManager = new GlobalManager();