diff options
| author | Mohamed Bassem <me@mbassem.com> | 2025-11-09 11:50:58 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-11-09 11:50:58 +0000 |
| commit | 3083be0c9dc9ec0ded58eda937b83fbdf511f386 (patch) | |
| tree | 43fd298af4ae45c5f7802e95af0a94cfd9745bc2 /apps/web | |
| parent | 1b8129a28191c7093818060e39e968fc16bf24b4 (diff) | |
| download | karakeep-3083be0c9dc9ec0ded58eda937b83fbdf511f386.tar.zst | |
feat: Add page titles (#2109)
* feat: add Next.js metadata titles to dynamic and settings pages
Add page titles using Next.js metadata API for better SEO and user experience:
- List pages: Show list name in format "<name> | Karakeep"
- Tag pages: Show tag name in format "<name> | Karakeep"
- Admin pages: Add titles for overview, users, and background jobs pages
- Settings pages: Add titles for all settings pages (API keys, AI, feeds, import, info, webhooks, subscription, rules, stats, assets, broken links)
For client components (rules, stats, assets, broken-links), created layout.tsx files to export metadata since metadata can only be exported from server components.
* feat: add Next.js metadata titles to dashboard pages
Add page titles using Next.js metadata API to archive, favourites, highlights, and all tags pages:
- Archive page: Show "Archive | Karakeep"
- Favourites page: Show "Favourites | Karakeep"
- Highlights page: Show "Highlights | Karakeep"
- All Tags page: Show "All Tags | Karakeep"
Improves SEO and user experience across all dashboard browsing pages.
* refactor: use i18n translations for dashboard page titles
Convert hardcoded page titles to use translations via generateMetadata:
- Archive page: Uses common.archive translation
- Favourites page: Uses lists.favourites translation
- Highlights page: Uses common.highlights translation
- All Tags page: Uses tags.all_tags translation
Improves localization support across dashboard pages.
* feat: add i18n translations to admin and settings page titles
Convert hardcoded page titles to use translations via generateMetadata:
- Admin Overview: Uses admin.admin_settings translation
- AI Settings: Uses settings.ai.ai_settings translation
- API Keys: Uses settings.api_keys.api_keys translation
- Feed Settings: Uses settings.feeds.rss_subscriptions translation
- Import/Export: Uses settings.import.import_export translation
- Account Info: Uses settings.info.user_info translation
- Subscription: Uses settings.subscription.subscription translation
- Webhooks: Uses settings.webhooks.webhooks translation
Improves localization support across admin and settings pages.
* revert accidental commit
* more translations
* more fixes
---------
Co-authored-by: Claude <noreply@anthropic.com>
Diffstat (limited to 'apps/web')
20 files changed, 247 insertions, 0 deletions
diff --git a/apps/web/app/admin/background_jobs/page.tsx b/apps/web/app/admin/background_jobs/page.tsx index 6a13dd64..e3958835 100644 --- a/apps/web/app/admin/background_jobs/page.tsx +++ b/apps/web/app/admin/background_jobs/page.tsx @@ -1,4 +1,14 @@ +import type { Metadata } from "next"; import BackgroundJobs from "@/components/admin/BackgroundJobs"; +import { useTranslation } from "@/lib/i18n/server"; + +export async function generateMetadata(): Promise<Metadata> { + // oxlint-disable-next-line rules-of-hooks + const { t } = await useTranslation(); + return { + title: `${t("admin.background_jobs.background_jobs")} | Karakeep`, + }; +} export default function BackgroundJobsPage() { return <BackgroundJobs />; diff --git a/apps/web/app/admin/overview/page.tsx b/apps/web/app/admin/overview/page.tsx index 66844d04..04d99b91 100644 --- a/apps/web/app/admin/overview/page.tsx +++ b/apps/web/app/admin/overview/page.tsx @@ -1,5 +1,15 @@ +import type { Metadata } from "next"; import BasicStats from "@/components/admin/BasicStats"; import ServiceConnections from "@/components/admin/ServiceConnections"; +import { useTranslation } from "@/lib/i18n/server"; + +export async function generateMetadata(): Promise<Metadata> { + // oxlint-disable-next-line rules-of-hooks + const { t } = await useTranslation(); + return { + title: `${t("admin.admin_settings")} | Karakeep`, + }; +} export default function AdminOverviewPage() { return ( diff --git a/apps/web/app/admin/users/page.tsx b/apps/web/app/admin/users/page.tsx index be5cfe81..5af899a4 100644 --- a/apps/web/app/admin/users/page.tsx +++ b/apps/web/app/admin/users/page.tsx @@ -1,4 +1,14 @@ +import type { Metadata } from "next"; import UserList from "@/components/admin/UserList"; +import { useTranslation } from "@/lib/i18n/server"; + +export async function generateMetadata(): Promise<Metadata> { + // oxlint-disable-next-line rules-of-hooks + const { t } = await useTranslation(); + return { + title: `${t("admin.users_list.users_list")} | Karakeep`, + }; +} export default function AdminUsersPage() { return <UserList />; diff --git a/apps/web/app/dashboard/archive/page.tsx b/apps/web/app/dashboard/archive/page.tsx index becb6a58..33a6e610 100644 --- a/apps/web/app/dashboard/archive/page.tsx +++ b/apps/web/app/dashboard/archive/page.tsx @@ -1,5 +1,15 @@ +import type { Metadata } from "next"; import Bookmarks from "@/components/dashboard/bookmarks/Bookmarks"; import InfoTooltip from "@/components/ui/info-tooltip"; +import { useTranslation } from "@/lib/i18n/server"; + +export async function generateMetadata(): Promise<Metadata> { + // oxlint-disable-next-line rules-of-hooks + const { t } = await useTranslation(); + return { + title: `${t("common.archive")} | Karakeep`, + }; +} function header() { return ( diff --git a/apps/web/app/dashboard/favourites/page.tsx b/apps/web/app/dashboard/favourites/page.tsx index be20bd2f..7ece9cdf 100644 --- a/apps/web/app/dashboard/favourites/page.tsx +++ b/apps/web/app/dashboard/favourites/page.tsx @@ -1,4 +1,14 @@ +import type { Metadata } from "next"; import Bookmarks from "@/components/dashboard/bookmarks/Bookmarks"; +import { useTranslation } from "@/lib/i18n/server"; + +export async function generateMetadata(): Promise<Metadata> { + // oxlint-disable-next-line rules-of-hooks + const { t } = await useTranslation(); + return { + title: `${t("lists.favourites")} | Karakeep`, + }; +} export default async function FavouritesBookmarkPage() { return ( diff --git a/apps/web/app/dashboard/highlights/page.tsx b/apps/web/app/dashboard/highlights/page.tsx index 1410d1fd..5945de00 100644 --- a/apps/web/app/dashboard/highlights/page.tsx +++ b/apps/web/app/dashboard/highlights/page.tsx @@ -1,9 +1,18 @@ +import type { Metadata } from "next"; import AllHighlights from "@/components/dashboard/highlights/AllHighlights"; import { Separator } from "@/components/ui/separator"; import { useTranslation } from "@/lib/i18n/server"; import { api } from "@/server/api/client"; import { Highlighter } from "lucide-react"; +export async function generateMetadata(): Promise<Metadata> { + // oxlint-disable-next-line rules-of-hooks + const { t } = await useTranslation(); + return { + title: `${t("common.highlights")} | Karakeep`, + }; +} + export default async function HighlightsPage() { // oxlint-disable-next-line rules-of-hooks const { t } = await useTranslation(); diff --git a/apps/web/app/dashboard/lists/[listId]/page.tsx b/apps/web/app/dashboard/lists/[listId]/page.tsx index 4714a71c..3f9c3416 100644 --- a/apps/web/app/dashboard/lists/[listId]/page.tsx +++ b/apps/web/app/dashboard/lists/[listId]/page.tsx @@ -1,3 +1,4 @@ +import type { Metadata } from "next"; import { notFound } from "next/navigation"; import Bookmarks from "@/components/dashboard/bookmarks/Bookmarks"; import ListHeader from "@/components/dashboard/lists/ListHeader"; @@ -6,6 +7,23 @@ import { TRPCError } from "@trpc/server"; import { BookmarkListContextProvider } from "@karakeep/shared-react/hooks/bookmark-list-context"; +export async function generateMetadata(props: { + params: Promise<{ listId: string }>; +}): Promise<Metadata> { + const params = await props.params; + try { + const list = await api.lists.get({ listId: params.listId }); + return { + title: `${list.name} | Karakeep`, + }; + } catch (e) { + if (e instanceof TRPCError && e.code === "NOT_FOUND") { + notFound(); + } + throw e; + } +} + export default async function ListPage(props: { params: Promise<{ listId: string }>; searchParams?: Promise<{ diff --git a/apps/web/app/dashboard/tags/[tagId]/page.tsx b/apps/web/app/dashboard/tags/[tagId]/page.tsx index 7971da1e..fcb5010e 100644 --- a/apps/web/app/dashboard/tags/[tagId]/page.tsx +++ b/apps/web/app/dashboard/tags/[tagId]/page.tsx @@ -1,3 +1,4 @@ +import type { Metadata } from "next"; import { notFound } from "next/navigation"; import Bookmarks from "@/components/dashboard/bookmarks/Bookmarks"; import EditableTagName from "@/components/dashboard/tags/EditableTagName"; @@ -7,6 +8,23 @@ import { api } from "@/server/api/client"; import { TRPCError } from "@trpc/server"; import { MoreHorizontal } from "lucide-react"; +export async function generateMetadata(props: { + params: Promise<{ tagId: string }>; +}): Promise<Metadata> { + const params = await props.params; + try { + const tag = await api.tags.get({ tagId: params.tagId }); + return { + title: `${tag.name} | Karakeep`, + }; + } catch (e) { + if (e instanceof TRPCError && e.code === "NOT_FOUND") { + notFound(); + } + throw e; + } +} + export default async function TagPage(props: { params: Promise<{ tagId: string }>; searchParams?: Promise<{ diff --git a/apps/web/app/dashboard/tags/page.tsx b/apps/web/app/dashboard/tags/page.tsx index b2acd45b..2ebcf2f7 100644 --- a/apps/web/app/dashboard/tags/page.tsx +++ b/apps/web/app/dashboard/tags/page.tsx @@ -1,4 +1,14 @@ +import type { Metadata } from "next"; import AllTagsView from "@/components/dashboard/tags/AllTagsView"; +import { useTranslation } from "@/lib/i18n/server"; + +export async function generateMetadata(): Promise<Metadata> { + // oxlint-disable-next-line rules-of-hooks + const { t } = await useTranslation(); + return { + title: `${t("tags.all_tags")} | Karakeep`, + }; +} export default async function TagsPage() { return <AllTagsView />; diff --git a/apps/web/app/settings/ai/page.tsx b/apps/web/app/settings/ai/page.tsx index 2b3d7a8d..02e2ae4b 100644 --- a/apps/web/app/settings/ai/page.tsx +++ b/apps/web/app/settings/ai/page.tsx @@ -1,4 +1,14 @@ +import type { Metadata } from "next"; import AISettings from "@/components/settings/AISettings"; +import { useTranslation } from "@/lib/i18n/server"; + +export async function generateMetadata(): Promise<Metadata> { + // oxlint-disable-next-line rules-of-hooks + const { t } = await useTranslation(); + return { + title: `${t("settings.ai.ai_settings")} | Karakeep`, + }; +} export default function AISettingsPage() { return <AISettings />; diff --git a/apps/web/app/settings/api-keys/page.tsx b/apps/web/app/settings/api-keys/page.tsx index 1c3718d6..494a0b5c 100644 --- a/apps/web/app/settings/api-keys/page.tsx +++ b/apps/web/app/settings/api-keys/page.tsx @@ -1,4 +1,14 @@ +import type { Metadata } from "next"; import ApiKeySettings from "@/components/settings/ApiKeySettings"; +import { useTranslation } from "@/lib/i18n/server"; + +export async function generateMetadata(): Promise<Metadata> { + // oxlint-disable-next-line rules-of-hooks + const { t } = await useTranslation(); + return { + title: `${t("settings.api_keys.api_keys")} | Karakeep`, + }; +} export default async function ApiKeysPage() { return ( diff --git a/apps/web/app/settings/assets/layout.tsx b/apps/web/app/settings/assets/layout.tsx new file mode 100644 index 00000000..2324c83a --- /dev/null +++ b/apps/web/app/settings/assets/layout.tsx @@ -0,0 +1,18 @@ +import type { Metadata } from "next"; +import { useTranslation } from "@/lib/i18n/server"; + +export async function generateMetadata(): Promise<Metadata> { + // oxlint-disable-next-line rules-of-hooks + const { t } = await useTranslation(); + return { + title: `${t("settings.manage_assets.manage_assets")} | Karakeep`, + }; +} + +export default function AssetsLayout({ + children, +}: { + children: React.ReactNode; +}) { + return <>{children}</>; +} diff --git a/apps/web/app/settings/broken-links/layout.tsx b/apps/web/app/settings/broken-links/layout.tsx new file mode 100644 index 00000000..45f3dadf --- /dev/null +++ b/apps/web/app/settings/broken-links/layout.tsx @@ -0,0 +1,18 @@ +import type { Metadata } from "next"; +import { useTranslation } from "@/lib/i18n/server"; + +export async function generateMetadata(): Promise<Metadata> { + // oxlint-disable-next-line rules-of-hooks + const { t } = await useTranslation(); + return { + title: `${t("settings.broken_links.broken_links")} | Karakeep`, + }; +} + +export default function BrokenLinksLayout({ + children, +}: { + children: React.ReactNode; +}) { + return <>{children}</>; +} diff --git a/apps/web/app/settings/feeds/page.tsx b/apps/web/app/settings/feeds/page.tsx index d2d1f182..603e9a09 100644 --- a/apps/web/app/settings/feeds/page.tsx +++ b/apps/web/app/settings/feeds/page.tsx @@ -1,4 +1,14 @@ +import type { Metadata } from "next"; import FeedSettings from "@/components/settings/FeedSettings"; +import { useTranslation } from "@/lib/i18n/server"; + +export async function generateMetadata(): Promise<Metadata> { + // oxlint-disable-next-line rules-of-hooks + const { t } = await useTranslation(); + return { + title: `${t("settings.feeds.rss_subscriptions")} | Karakeep`, + }; +} export default function FeedSettingsPage() { return <FeedSettings />; diff --git a/apps/web/app/settings/import/page.tsx b/apps/web/app/settings/import/page.tsx index 11780d51..23ef4019 100644 --- a/apps/web/app/settings/import/page.tsx +++ b/apps/web/app/settings/import/page.tsx @@ -1,4 +1,14 @@ +import type { Metadata } from "next"; import ImportExport from "@/components/settings/ImportExport"; +import { useTranslation } from "@/lib/i18n/server"; + +export async function generateMetadata(): Promise<Metadata> { + // oxlint-disable-next-line rules-of-hooks + const { t } = await useTranslation(); + return { + title: `${t("settings.import.import_export")} | Karakeep`, + }; +} export default function ImportSettingsPage() { return <ImportExport />; diff --git a/apps/web/app/settings/info/page.tsx b/apps/web/app/settings/info/page.tsx index 96deab96..1807b538 100644 --- a/apps/web/app/settings/info/page.tsx +++ b/apps/web/app/settings/info/page.tsx @@ -1,7 +1,17 @@ +import type { Metadata } from "next"; import { ChangePassword } from "@/components/settings/ChangePassword"; import { DeleteAccount } from "@/components/settings/DeleteAccount"; import UserDetails from "@/components/settings/UserDetails"; import UserOptions from "@/components/settings/UserOptions"; +import { useTranslation } from "@/lib/i18n/server"; + +export async function generateMetadata(): Promise<Metadata> { + // oxlint-disable-next-line rules-of-hooks + const { t } = await useTranslation(); + return { + title: `${t("settings.info.user_info")} | Karakeep`, + }; +} export default async function InfoPage() { return ( diff --git a/apps/web/app/settings/rules/layout.tsx b/apps/web/app/settings/rules/layout.tsx new file mode 100644 index 00000000..03b4316e --- /dev/null +++ b/apps/web/app/settings/rules/layout.tsx @@ -0,0 +1,18 @@ +import type { Metadata } from "next"; +import { useTranslation } from "@/lib/i18n/server"; + +export async function generateMetadata(): Promise<Metadata> { + // oxlint-disable-next-line rules-of-hooks + const { t } = await useTranslation(); + return { + title: `${t("settings.rules.rules")} | Karakeep`, + }; +} + +export default function RulesLayout({ + children, +}: { + children: React.ReactNode; +}) { + return <>{children}</>; +} diff --git a/apps/web/app/settings/stats/layout.tsx b/apps/web/app/settings/stats/layout.tsx new file mode 100644 index 00000000..0f24c049 --- /dev/null +++ b/apps/web/app/settings/stats/layout.tsx @@ -0,0 +1,18 @@ +import type { Metadata } from "next"; +import { useTranslation } from "@/lib/i18n/server"; + +export async function generateMetadata(): Promise<Metadata> { + // oxlint-disable-next-line rules-of-hooks + const { t } = await useTranslation(); + return { + title: `${t("settings.stats.usage_statistics")} | Karakeep`, + }; +} + +export default function StatsLayout({ + children, +}: { + children: React.ReactNode; +}) { + return <>{children}</>; +} diff --git a/apps/web/app/settings/subscription/page.tsx b/apps/web/app/settings/subscription/page.tsx index e8c46460..5b06798e 100644 --- a/apps/web/app/settings/subscription/page.tsx +++ b/apps/web/app/settings/subscription/page.tsx @@ -1,9 +1,19 @@ +import type { Metadata } from "next"; import { redirect } from "next/navigation"; import SubscriptionSettings from "@/components/settings/SubscriptionSettings"; import { QuotaProgress } from "@/components/subscription/QuotaProgress"; +import { useTranslation } from "@/lib/i18n/server"; import serverConfig from "@karakeep/shared/config"; +export async function generateMetadata(): Promise<Metadata> { + // oxlint-disable-next-line rules-of-hooks + const { t } = await useTranslation(); + return { + title: `${t("settings.subscription.subscription")} | Karakeep`, + }; +} + export default async function SubscriptionPage() { if (!serverConfig.stripe.isConfigured) { redirect("/settings"); diff --git a/apps/web/app/settings/webhooks/page.tsx b/apps/web/app/settings/webhooks/page.tsx index 327d0d8f..4f798aa1 100644 --- a/apps/web/app/settings/webhooks/page.tsx +++ b/apps/web/app/settings/webhooks/page.tsx @@ -1,4 +1,14 @@ +import type { Metadata } from "next"; import WebhookSettings from "@/components/settings/WebhookSettings"; +import { useTranslation } from "@/lib/i18n/server"; + +export async function generateMetadata(): Promise<Metadata> { + // oxlint-disable-next-line rules-of-hooks + const { t } = await useTranslation(); + return { + title: `${t("settings.webhooks.webhooks")} | Karakeep`, + }; +} export default function WebhookSettingsPage() { return <WebhookSettings />; |
