fix: address branding review feedback

This commit is contained in:
ajnart committed 2026-08-24 07:58:48 +02:00
1 parent 346bead673
commit 2613a556b1
22 files changed
+61 -59

No files matched your search

@@ -16,7 +16,6 @@ import type { WidgetKind } from "@homarr/definitions";
import { getI18n } from "@homarr/translation/server";
import { prefetchForKindAsync } from "@homarr/widgets/prefetch";
import { createMetaTitle } from "~/metadata";
import { env } from "~/env";
import { createBoardLayout } from "../_layout-creator";
import type { Board, Item } from "../_types";
@@ -115,9 +114,7 @@ export const createBoardContentPage = <TParams extends Record<string, unknown>>(
const favicon = !isNullOrWhitespace(board.faviconImageUrl) ? board.faviconImageUrl : brandFavicon;
return {
title: board.metaTitle
? { absolute: board.metaTitle }
: createMetaTitle(t("content.metaTitle", { boardName: board.name })),
title: board.metaTitle ? { absolute: board.metaTitle } : t("content.metaTitle", { boardName: board.name }),
icons: {
icon: favicon,
apple: favicon,
@@ -11,7 +11,6 @@ import { useI18n } from "@homarr/translation/client";
import { useSettings } from "@homarr/settings";
import { SectionCard } from "~/components/manage/section-card";
import { createMetaTitle } from "~/metadata";
import type { Board } from "../../_types";
import type { FormValues } from "./_settings-form";
@@ -23,7 +22,7 @@ interface Props {
export const GeneralSettingsContent = ({ board, form }: Props) => {
const t = useI18n("board");
const { branding } = useSettings();
const defaultMetaTitle = createMetaTitle(t("content.metaTitle", { boardName: board.name }), branding.appName);
const defaultMetaTitle = t("content.metaTitle", { boardName: board.name });
useLogoPreview(form.values.logoImageUrl);
const metaTitleStatus = useMetaTitlePreview(form.values.metaTitle, defaultMetaTitle);
+10 -9
View File
@@ -27,6 +27,7 @@ import { SettingsProvider } from "@homarr/settings";
import { SpotlightProvider } from "@homarr/spotlight";
import type { SupportedLanguage } from "@homarr/translation";
import { isLocaleRTL, isLocaleSupported } from "@homarr/translation";
import { getI18n } from "@homarr/translation/server";
import { resolveHomarrUrlConfig } from "@homarr/workshop/schema";
import { Analytics } from "~/components/layout/analytics";
@@ -53,25 +54,26 @@ const fontSans = Inter({
const logger = createLogger({ module: "rootLayout" });
const defaultDescription =
"A self-hosted dashboard for the *arr stack and your entire homelab. Integrates with 50+ services, real-time widgets, no config files.";
// eslint-disable-next-line no-restricted-syntax
export const generateMetadata = async (): Promise<Metadata> => {
const [serverSettings, colorScheme] = await Promise.all([getRscServerSettingsAsync(), getCurrentColorSchemeAsync()]);
const [serverSettings, colorScheme, t] = await Promise.all([
getRscServerSettingsAsync(),
getCurrentColorSchemeAsync(),
getI18n("metadata"),
]);
const { appName, faviconImageUrl, logoImageUrl } = serverSettings.branding;
const logo = logoImageUrl ?? "/logo/logo.png";
const favicon = faviconImageUrl ?? logo;
const description = t("description");
return {
title: {
default: appName,
template: `%s • ${appName}`,
},
description: defaultDescription,
description,
openGraph: {
title: `${appName} Dashboard`,
description: defaultDescription,
title: t("dashboardTitle", { appName }),
description,
url: env.HOMARR_WEBSITE_URL,
siteName: appName,
},
@@ -88,7 +90,6 @@ export const generateMetadata = async (): Promise<Metadata> => {
};
};
// eslint-disable-next-line no-restricted-syntax
export const generateViewport = async (): Promise<Viewport> => {
const serverSettings = await getRscServerSettingsAsync();
return { themeColor: serverSettings.branding.primaryColor };
@@ -39,7 +39,6 @@ import { getI18n } from "@homarr/translation/server";
import { homarrLogoPath } from "~/components/layout/logo/constants";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { createMetaTitle } from "~/metadata";
import { getDependenciesAsync, getPackageVersion } from "~/versions/package-reader";
import githubContributors from "@static-data/contributors.json";
import openCollectiveContributors from "@static-data/opencollective-contributors.json";
@@ -50,7 +49,7 @@ export async function generateMetadata() {
const t = await getI18n("management");
return {
title: createMetaTitle(t("metaTitle")),
title: t("metaTitle"),
};
}
@@ -5,13 +5,12 @@ import { auth } from "@homarr/auth/next";
import { getI18n } from "@homarr/translation/server";
import { ManagePageLayout } from "~/components/manage/manage-page-layout";
import { createMetaTitle } from "~/metadata";
import { AssistantConfiguration } from "./_components/assistant-configuration";
export async function generateMetadata() {
const t = await getI18n("management.page.settings.section.assistant");
return {
title: createMetaTitle(t("title")),
title: t("title"),
};
}
+1 -2
View File
@@ -7,14 +7,13 @@ import { getI18n } from "@homarr/translation/server";
import { Link } from "@homarr/ui";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { createMetaTitle } from "~/metadata";
import { HeroBanner } from "./_components/hero-banner";
export async function generateMetadata(): Promise<Metadata> {
const t = await getI18n("management");
return {
title: createMetaTitle(t("metaTitle")),
title: t("metaTitle"),
};
}
@@ -93,10 +93,7 @@ export const BrandingSettingsForm = ({ form }: BrandingSettingsFormProps) => {
value={branding.defaultRadius}
labels={
Object.fromEntries(
cornerStyleValues.map((cornerStyle) => [
cornerStyle,
t(`defaultRadius.options.${cornerStyle}` as never),
]),
cornerStyleValues.map((cornerStyle) => [cornerStyle, t(`defaultRadius.options.${cornerStyle}`)]),
) as Record<(typeof cornerStyleValues)[number], string>
}
onChange={(cornerStyle) => form.setFieldValue("branding.defaultRadius", cornerStyle)}
@@ -190,7 +187,7 @@ const BrandingSummary = ({ form }: BrandingSettingsFormProps) => {
<Group justify="space-between" gap="xs" wrap="wrap">
<Text fw={650}>{t("summary.title")}</Text>
<Badge variant="light">
{t("summary.cornerStyle")}: {t(`defaultRadius.options.${branding.defaultRadius}` as never)}
{t("summary.cornerStyle")}: {t(`defaultRadius.options.${branding.defaultRadius}`)}
</Badge>
</Group>
<SimpleGrid cols={{ base: 1, sm: 2, xl: 3 }} spacing="md" verticalSpacing="sm">
@@ -5,14 +5,13 @@ import { auth } from "@homarr/auth/next";
import { getI18n } from "@homarr/translation/server";
import { ManagePageLayout } from "~/components/manage/manage-page-layout";
import { createMetaTitle } from "~/metadata";
import { SettingsForm } from "./_components/settings-form";
export async function generateMetadata() {
const t = await getI18n("management");
return {
title: createMetaTitle(t("metaTitle")),
title: t("metaTitle"),
};
}
@@ -9,7 +9,6 @@ import { auth } from "@homarr/auth/next";
import { extractBaseUrlFromHeaders } from "@homarr/common";
import { getI18n } from "@homarr/translation/server";
import { createMetaTitle } from "~/metadata";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { extractMcpTools } from "~/app/api/mcp/_extract-tools";
import { ApiKeysManagement } from "./components/api-keys";
@@ -63,7 +62,7 @@ export async function generateMetadata() {
const t = await getI18n("management");
return {
title: createMetaTitle(t("metaTitle")),
title: t("metaTitle"),
};
}
@@ -5,7 +5,6 @@ import { auth } from "@homarr/auth/next";
import { dbEnv } from "@homarr/core/infrastructure/db/env";
import { getI18n } from "@homarr/translation/server";
import { createMetaTitle } from "~/metadata";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { BackupExportCard } from "./_components/backup-export-card";
import { BackupImportCard } from "./_components/backup-import-card";
@@ -19,7 +18,7 @@ export async function generateMetadata() {
const t = await getI18n("management.page.tool.backup");
return {
title: createMetaTitle(t("metaTitle")),
title: t("metaTitle"),
};
}
@@ -11,7 +11,6 @@ import { logsEnv } from "@homarr/core/infrastructure/logs/env";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { fullHeightWithoutHeaderAndFooter } from "~/constants";
import { createMetaTitle } from "~/metadata";
import { ClientSideTerminalComponent } from "./client";
import { LogFontSizeSlider } from "./font-size-slider";
import { LogLevelSelection } from "./level-selection";
@@ -25,7 +24,7 @@ export async function generateMetadata() {
const t = await getI18n("management");
return {
title: createMetaTitle(t("metaTitle")),
title: t("metaTitle"),
};
}
@@ -6,7 +6,6 @@ import { auth } from "@homarr/auth/next";
import { getI18n } from "@homarr/translation/server";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { createMetaTitle } from "~/metadata";
import { TasksTable } from "./_components/tasks-table";
export async function generateMetadata() {
@@ -17,7 +16,7 @@ export async function generateMetadata() {
const t = await getI18n("management");
return {
title: createMetaTitle(t("metaTitle")),
title: t("metaTitle"),
};
}
@@ -8,7 +8,6 @@ import { getI18n } from "@homarr/translation/server";
import { DangerZoneItem, DangerZoneRoot } from "~/components/manage/danger-zone";
import { catchTrpcNotFound } from "~/errors/trpc-catch-error";
import { createMetaTitle } from "~/metadata";
import { canAccessUserEditPage } from "../access";
import { DeleteUserButton } from "./_components/_delete-user-button";
import { UserGeneralSettingsForm } from "./_components/_general-settings-form";
@@ -38,7 +37,7 @@ export async function generateMetadata(props: Props) {
return {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
title: createMetaTitle(t("metaTitle", { username: user.name! })),
title: t("metaTitle", { username: user.name! }),
};
}
@@ -8,7 +8,6 @@ import { everyoneGroup } from "@homarr/definitions";
import { getI18n } from "@homarr/translation/server";
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { createMetaTitle } from "~/metadata";
import { UserCreateStepperComponent } from "./_components/create-user-stepper";
export async function generateMetadata() {
@@ -22,7 +21,7 @@ export async function generateMetadata() {
const t = await getI18n("management.page.user.create");
return {
title: createMetaTitle(t("metaTitle")),
title: t("metaTitle"),
};
}
@@ -9,7 +9,6 @@ import { Link } from "@homarr/ui";
import { TourTarget } from "~/components/layout/header/tour-target";
import { ManagePageLayout } from "~/components/manage/manage-page-layout";
import { MobileAffixButton } from "~/components/manage/mobile-affix-button";
import { createMetaTitle } from "~/metadata";
import { UserListComponent } from "./_components/user-list";
export async function generateMetadata() {
@@ -20,7 +19,7 @@ export async function generateMetadata() {
const t = await getI18n("management.page.user.list");
return {
title: createMetaTitle(t("metaTitle")),
title: t("metaTitle"),
};
}
@@ -4,6 +4,7 @@ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
import type { MetadataExtractor } from "@ai-sdk/openai-compatible";
import type { ToolSet, UIMessage } from "ai";
import { createUIMessageStream, createUIMessageStreamResponse, jsonSchema, stepCountIs, streamText, tool } from "ai";
import { cookies } from "next/headers";
import { z } from "zod/v4";
import { createTRPCContext, mcpRouter } from "@homarr/api/mcp";
@@ -24,6 +25,10 @@ import {
assistantProviderRequiresApiKey,
assistantReasoningModes,
} from "@homarr/definitions";
import { localeCookieKey } from "@homarr/definitions/cookie";
import type { SupportedLanguage } from "@homarr/translation";
import { fallbackLocale, isLocaleSupported } from "@homarr/translation";
import { getI18n } from "@homarr/translation/server";
import { resolveHomarrUrlConfig } from "@homarr/workshop/schema";
import { browserToolContracts } from "~/components/assistant/assistant-tool-contracts";
@@ -414,7 +419,11 @@ export async function POST(request: Request) {
const requiresApiKey = configuration ? assistantProviderRequiresApiKey(configuration.provider) : false;
if (!configuration?.enabled || !configuration.modelId || (requiresApiKey && !configuration.encryptedApiKey)) {
return Response.json({ error: "Assistant is not configured." }, { status: 503 });
const requestedLocale = (await cookies()).get(localeCookieKey)?.value;
let locale: SupportedLanguage = fallbackLocale;
if (requestedLocale && isLocaleSupported(requestedLocale)) locale = requestedLocale;
const t = await getI18n({ locale, namespace: "assistant.unavailable" });
return Response.json({ error: t("notConfigured") }, { status: 503 });
}
let homarrProviderToken: string | null | undefined;
@@ -10,7 +10,6 @@
width: 100%;
height: 100%;
padding-inline: var(--mantine-spacing-md);
direction: ltr;
}
.zone {
@@ -52,7 +51,7 @@
border-radius: var(--mantine-radius-md);
}
.logo :global(img.logo) {
.logo img {
display: block;
width: auto;
height: auto;
@@ -65,7 +64,9 @@
flex-wrap: nowrap;
}
.logo :global(.mantine-Title-root) {
.logo h1,
.logo h2,
.logo h3 {
display: none;
}
@@ -84,7 +85,7 @@
max-width: 100%;
}
.zone :global([data-tour-target="board-search"]) {
.zone [data-tour-target="board-search"] {
min-width: 0;
max-width: 100%;
}
@@ -119,7 +120,7 @@
transform: translateY(0) scale(1);
}
.floatingControls :global(.mantine-UnstyledButton-root) {
.floatingControls button {
display: flex;
align-items: center;
justify-content: center;
-4
View File
@@ -1,4 +0,0 @@
export const createMetaTitle = (name: string, appName?: string) => {
if (appName) return `${name} • ${appName}`;
return name;
};
+8 -4
View File
@@ -42,10 +42,14 @@ export const serverSettingsRouter = createTRPCRouter({
getAll: permissionRequiredProcedure.requiresPermission("admin").query(async ({ ctx }) => {
return await getServerSettingsAsync(ctx.db);
}),
getBranding: publicProcedure.query(async ({ ctx }) => {
const branding = await getServerSettingByKeyAsync(ctx.db, "branding");
return parseBrandingSettings(branding);
}),
getBranding: publicProcedure
.meta({
mcp: { enabled: true, description: "Returns the public instance branding configuration." },
})
.query(async ({ ctx }) => {
const branding = await getServerSettingByKeyAsync(ctx.db, "branding");
return parseBrandingSettings(branding);
}),
getBoardSettings: permissionRequiredProcedure
.requiresPermission("admin")
.meta({
+7 -2
View File
@@ -727,8 +727,6 @@ export const userRouter = createTRPCRouter({
.where(eq(users.id, input.id));
}),
changeHeaderPreferences: protectedProcedure
.input(z.object({ id: z.string(), headerPreferences: headerPreferencesMutationSchema }))
.output(z.void())
.meta({
openapi: {
method: "PATCH",
@@ -736,7 +734,14 @@ export const userRouter = createTRPCRouter({
tags: ["users"],
protect: true,
},
mcp: {
enabled: true,
description:
"Update the header layout preferences of a user. REQUIRED: id (user ID), headerPreferences (zones for left, center and right, visible flag and searchDisplay). Admins can change any user; other users can only change their own. Board shortcut items must reference boards the target user can view",
},
})
.input(z.object({ id: z.string(), headerPreferences: headerPreferencesMutationSchema }))
.output(z.void())
.mutation(async ({ input, ctx }) => {
if (!ctx.session.user.permissions.includes("admin") && ctx.session.user.id !== input.id) {
throw new TRPCError({
+1 -1
View File
@@ -1464,7 +1464,7 @@ const BoardBuilder = (props: StudioSectionProps) => {
value={props.itemRadius}
labels={
Object.fromEntries(
cornerStyleValues.map((cornerStyle) => [cornerStyle, t(`radiusOptions.${cornerStyle}` as never)]),
cornerStyleValues.map((cornerStyle) => [cornerStyle, t(`radiusOptions.${cornerStyle}`)]),
) as Record<CornerStyle, string>
}
onChange={props.setItemRadius}
+4
View File
@@ -7597,6 +7597,10 @@
}
}
},
"metadata": {
"description": "A self-hosted dashboard for the *arr stack and your entire homelab. Integrates with 50+ services, real-time widgets, no config files.",
"dashboardTitle": "{appName} Dashboard"
},
"assistant": {
"title": "Assistant",
"open": "Open Assistant",